Your First CLI¶
To get started with tyro
, let’s compare it with a traditional argparse
-based command-line
interface:
"""Sum two numbers from argparse."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--a", type=int, required=True)
parser.add_argument("--b", type=int, default=3)
args = parser.parse_args()
total = args.a + args.b
print(total)
This is dramatically cleaner than manually parsing sys.argv
, but has several
issues:
It requires significant parsing-specific boilerplate.
It lacks type checking and IDE support (no jumping to definitions, finding references, docstrings, refactoring or renaming tools).
It becomes difficult to manage for larger projects with many parameters.
It doesn’t automatically generate comprehensive help text.
tyro.cli()
aims to solve these issues by building CLIs directly from type annotations.
Command-line interfaces from functions¶
We can write the same script as above using tyro.cli()
:
"""Sum two numbers by calling a function with tyro."""
import tyro
def add(a: int, b: int = 3) -> int:
"""Add two numbers together.
Args:
a: First number to add.
b: Second number to add. Defaults to 3.
"""
return a + b
# Populate the inputs of add(), call it, then return the output.
total = tyro.cli(add)
print(total)
Using this script from the command line would look like:
$ python script.py --a 5
8
$ python script.py --a 5 --b 7
12
$ python script.py --help
usage: script.py [-h] --a INT [--b INT]
Add two numbers together.
╭─ options ───────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --a INT First number to add. (required) │
│ --b INT Second number to add. Defaults to 3. (default: 3) │
╰─────────────────────────────────────────────────────────────────────╯
A more succinct version that combines the function call with printing:
"""Sum two numbers by calling a function with tyro."""
import tyro
def add(a: int, b: int = 3) -> None:
"""Add two numbers together and print the result."""
print(a + b)
tyro.cli(add) # Parses arguments, calls add(), and returns None.
Command-line interfaces from config objects¶
A class in Python can be treated as a function that returns an instance. This is particularly useful for more complex configurations:
"""Sum two numbers by instantiating a dataclass with tyro."""
from dataclasses import dataclass
import tyro
@dataclass
class Args:
"""Configuration for adding two numbers."""
a: int # First number to add
b: int = 3 # Second number to add (default: 3)
args = tyro.cli(Args)
print(args.a + args.b)
From the command line, this would look identical to the function example:
$ python script.py --a 5
8
$ python script.py --help
usage: script.py [-h] --a INT [--b INT]
Configuration for adding two numbers.
╭─ options ────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --a INT First number to add (required) │
│ --b INT Second number to add (default: 3) (default: 3) │
╰──────────────────────────────────────────────────────────────────╯
Benefits over argparse¶
Unlike directly using argparse
, both the function-based and dataclass-based
approaches provide:
Static type checking - Parameters have real types that can be checked.
IDE support - Jump to definitions, find references, see docstrings, and use refactoring tools.
Automatic helptext - Generated from docstrings and comments.
Less boilerplate - No need to manually define every argument.
Hierarchical configuration - Easily nest parameters in complex structures.
By incorporating standard type annotations, tyro can handle a broad range of advanced behaviors: nested structures, variable-length inputs, unions over types, subcommands, and more. Our examples walk through these features in detail.