Subcommands

In these examples, we show how tyro.cli() can be used to create CLI interfaces with subcommands.

Subcommands are Unions

All of tyro’s subcommand features are built using unions over struct types (typically dataclasses). Subcommands are used to choose between types in the union; arguments are then populated from the chosen type.

Note

For configuring subcommands beyond what can be expressed with type annotations, see tyro.conf.subcommand().

 1# 01_subcommands.py
 2from __future__ import annotations
 3
 4import dataclasses
 5
 6import tyro
 7
 8@dataclasses.dataclass(frozen=True)
 9class Checkout:
10    """Checkout a branch."""
11
12    branch: str
13
14@dataclasses.dataclass(frozen=True)
15class Commit:
16    """Commit changes."""
17
18    message: str
19
20if __name__ == "__main__":
21    cmd = tyro.cli(Checkout | Commit)
22    print(cmd)

Print the helptext. This will show the available subcommands:

$ python ./01_subcommands.py --help
usage: 01_subcommands.py [-h] {checkout,commit}

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
╰─────────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────────╮
 {checkout,commit}                                       
     checkout            Checkout a branch.              
     commit              Commit changes.                 
╰─────────────────────────────────────────────────────────╯

The commit subcommand:

$ python ./01_subcommands.py commit --help
usage: 01_subcommands.py commit [-h] --message STR

Commit changes.

╭─ options ────────────────────────────────────────────╮
 -h, --help           show this help message and exit 
 --message STR        (required)                      
╰──────────────────────────────────────────────────────╯
$ python ./01_subcommands.py commit --message hello
Commit(message='hello')

The checkout subcommand:

$ python ./01_subcommands.py checkout --help
usage: 01_subcommands.py checkout [-h] --branch STR

Checkout a branch.

╭─ options ───────────────────────────────────────────╮
 -h, --help          show this help message and exit 
 --branch STR        (required)                      
╰─────────────────────────────────────────────────────╯
$ python ./01_subcommands.py checkout --branch main
Checkout(branch='main')

Subcommands as Function Arguments

A subcommand will be created for each input annotated with a union over struct types.

Note

To prevent tyro.cli() from converting a Union type into a subcommand, use tyro.conf.AvoidSubcommands.

Note

Argument ordering for subcommands can be tricky. In the example below, --shared-arg must always come before the subcommand. As an option for alleviating this, see tyro.conf.ConsolidateSubcommandArgs.

 1# 02_subcommands_in_func.py
 2from __future__ import annotations
 3
 4import dataclasses
 5
 6import tyro
 7
 8@dataclasses.dataclass(frozen=True)
 9class Checkout:
10    """Checkout a branch."""
11
12    branch: str
13
14@dataclasses.dataclass(frozen=True)
15class Commit:
16    """Commit changes."""
17
18    message: str
19
20def main(
21    shared_arg: int,
22    cmd: Checkout | Commit = Checkout(branch="default"),
23):
24    print(f"{shared_arg=}")
25    print(cmd)
26
27if __name__ == "__main__":
28    tyro.cli(main)

Print the helptext. This will show the available subcommands:

$ python ./02_subcommands_in_func.py --help
usage: 02_subcommands_in_func.py [-h] --shared-arg INT
                                 [{cmd:checkout,cmd:commit}]

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
 --shared-arg INT        (required)                      
╰─────────────────────────────────────────────────────────╯
╭─ optional subcommands ──────────────────────────────────╮
 (default: cmd:checkout)                                 
 ──────────────────────────────────────────              
 [{cmd:checkout,cmd:commit}]                             
     cmd:checkout        Checkout a branch.              
     cmd:commit          Commit changes.                 
╰─────────────────────────────────────────────────────────╯

Using the default subcommand:

$ python ./02_subcommands_in_func.py --shared-arg 100
shared_arg=100
Checkout(branch='default')

Choosing a different subcommand:

$ python ./02_subcommands_in_func.py --shared-arg 100 cmd:commit --cmd.message 'Hello!'
shared_arg=100
Commit(message='Hello!')

Sequenced Subcommands

Multiple unions over struct types are populated using a series of subcommands.

 1# 03_multiple_subcommands.py
 2from __future__ import annotations
 3
 4import dataclasses
 5from typing import Literal
 6
 7import tyro
 8
 9# Possible dataset configurations.
10
11@dataclasses.dataclass
12class Mnist:
13    binary: bool = False
14    """Set to load binary version of MNIST dataset."""
15
16@dataclasses.dataclass
17class ImageNet:
18    subset: Literal[50, 100, 1000]
19    """Choose between ImageNet-50, ImageNet-100, ImageNet-1000, etc."""
20
21# Possible optimizer configurations.
22
23@dataclasses.dataclass
24class Adam:
25    learning_rate: float = 1e-3
26    betas: tuple[float, float] = (0.9, 0.999)
27
28@dataclasses.dataclass
29class Sgd:
30    learning_rate: float = 3e-4
31
32# Train script.
33
34def train(
35    dataset: Mnist | ImageNet = Mnist(),
36    optimizer: Adam | Sgd = Adam(),
37) -> None:
38    """Example training script.
39
40    Args:
41        dataset: Dataset to train on.
42        optimizer: Optimizer to train with.
43
44    Returns:
45        None:
46    """
47    print(dataset)
48    print(optimizer)
49
50if __name__ == "__main__":
51    tyro.cli(train, config=(tyro.conf.ConsolidateSubcommandArgs,))

We apply the tyro.conf.ConsolidateSubcommandArgs flag. This pushes all arguments to the end of the command:

$ python ./03_multiple_subcommands.py --help
usage: 03_multiple_subcommands.py [-h] {dataset:mnist,dataset:image-net}

Example training script.

╭─ options ─────────────────────────────────────────╮
 -h, --help        show this help message and exit 
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
 Dataset to train on.                              
 ─────────────────────────────────                 
 {dataset:mnist,dataset:image-net}                 
     dataset:mnist                                 
     dataset:image-net                             
╰───────────────────────────────────────────────────╯
$ python ./03_multiple_subcommands.py dataset:mnist --help
usage: 03_multiple_subcommands.py dataset:mnist [-h]
                                                {optimizer:adam,optimizer:sgd}

╭─ options ─────────────────────────────────────────╮
 -h, --help        show this help message and exit 
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
 Optimizer to train with.                          
 ──────────────────────────────                    
 {optimizer:adam,optimizer:sgd}                    
     optimizer:adam                                
     optimizer:sgd                                 
╰───────────────────────────────────────────────────╯
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --help
usage: 03_multiple_subcommands.py dataset:mnist optimizer:adam
       [-h] [--optimizer.learning-rate FLOAT] [--optimizer.betas FLOAT FLOAT]
       [--dataset.binary | --dataset.no-binary]

╭─ options ─────────────────────────────────────────────────────────╮
 -h, --help                                                        
     show this help message and exit                               
╰───────────────────────────────────────────────────────────────────╯
╭─ optimizer options ───────────────────────────────────────────────╮
 --optimizer.learning-rate FLOAT                                   
     (default: 0.001)                                              
 --optimizer.betas FLOAT FLOAT                                     
     (default: 0.9 0.999)                                          
╰───────────────────────────────────────────────────────────────────╯
╭─ dataset options ─────────────────────────────────────────────────╮
 --dataset.binary, --dataset.no-binary                             
     Set to load binary version of MNIST dataset. (default: False) 
╰───────────────────────────────────────────────────────────────────╯
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --optimizer.learning-rate 3e-4 --dataset.binary
Mnist(binary=True)
Adam(learning_rate=0.0003, betas=(0.9, 0.999))

Decorator-based Subcommands

tyro.extras.SubcommandApp() provides a decorator-based API for subcommands, which is inspired by click.

 1# 04_decorator_subcommands.py
 2from tyro.extras import SubcommandApp
 3
 4app = SubcommandApp()
 5
 6@app.command
 7def greet(name: str, loud: bool = False) -> None:
 8    """Greet someone."""
 9    greeting = f"Hello, {name}!"
10    if loud:
11        greeting = greeting.upper()
12    print(greeting)
13
14@app.command(name="addition")
15def add(a: int, b: int) -> None:
16    """Add two numbers."""
17    print(f"{a} + {b} = {a + b}")
18
19if __name__ == "__main__":
20    app.cli()
$ python 04_decorator_subcommands.py --help
usage: 04_decorator_subcommands.py [-h] {greet,addition}

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
╰─────────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────────╮
 {greet,addition}                                        
     greet               Greet someone.                  
     addition            Add two numbers.                
╰─────────────────────────────────────────────────────────╯
$ python 04_decorator_subcommands.py greet --help
usage: 04_decorator_subcommands.py greet [-h] --name STR [--loud | --no-loud]

Greet someone.

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
 --name STR              (required)                      
 --loud, --no-loud       (default: False)                
╰─────────────────────────────────────────────────────────╯
$ python 04_decorator_subcommands.py greet --name Alice
Hello, Alice!
$ python 04_decorator_subcommands.py greet --name Bob --loud
HELLO, BOB!
$ python 04_decorator_subcommands.py addition --help
usage: 04_decorator_subcommands.py addition [-h] --a INT --b INT

Add two numbers.

╭─ options ─────────────────────────────────────────╮
 -h, --help        show this help message and exit 
 --a INT           (required)                      
 --b INT           (required)                      
╰───────────────────────────────────────────────────╯
$ python 04_decorator_subcommands.py addition --a 5 --b 3
5 + 3 = 8

Subcommands from Functions

We provide a shorthand for generating a subcommand CLI from a dictionary. This is a thin wrapper around tyro.cli()’s more verbose, type-based API. If more generality is needed, the internal working are explained in the docs for tyro.extras.subcommand_cli_from_dict().

 1# 05_subcommands_func.py
 2import tyro
 3
 4def checkout(branch: str) -> None:
 5    """Check out a branch."""
 6    print(f"{branch=}")
 7
 8def commit(message: str, all: bool = False) -> None:
 9    """Make a commit."""
10    print(f"{message=} {all=}")
11
12if __name__ == "__main__":
13    tyro.extras.subcommand_cli_from_dict(
14        {
15            "checkout": checkout,
16            "commit": commit,
17        }
18    )
$ python ./05_subcommands_func.py --help
usage: 05_subcommands_func.py [-h] {checkout,commit}

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
╰─────────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────────╮
 {checkout,commit}                                       
     checkout            Check out a branch.             
     commit              Make a commit.                  
╰─────────────────────────────────────────────────────────╯
$ python ./05_subcommands_func.py commit --help
usage: 05_subcommands_func.py commit [-h] --message STR [--all | --no-all]

Make a commit.

╭─ options ──────────────────────────────────────────────╮
 -h, --help             show this help message and exit 
 --message STR          (required)                      
 --all, --no-all        (default: False)                
╰────────────────────────────────────────────────────────╯
$ python ./05_subcommands_func.py commit --message hello --all
message='hello' all=True
$ python ./05_subcommands_func.py checkout --help
usage: 05_subcommands_func.py checkout [-h] --branch STR

Check out a branch.

╭─ options ───────────────────────────────────────────╮
 -h, --help          show this help message and exit 
 --branch STR        (required)                      
╰─────────────────────────────────────────────────────╯
$ python ./05_subcommands_func.py checkout --branch main
branch='main'