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:

  1. Static type checking - Parameters have real types that can be checked.

  2. IDE support - Jump to definitions, find references, see docstrings, and use refactoring tools.

  3. Automatic helptext - Generated from docstrings and comments.

  4. Less boilerplate - No need to manually define every argument.

  5. 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.