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
 9class Checkout:
10    """Checkout a branch."""
11
12    branch: str
13
14@dataclasses.dataclass
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 ─────────────────────────────────╮
 (required)                                    
   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.CascadeSubcommandArgs.

 1# 02_subcommands_in_func.py
 2from __future__ import annotations
 3
 4import dataclasses
 5
 6import tyro
 7
 8@dataclasses.dataclass
 9class Checkout:
10    """Checkout a branch."""
11
12    branch: str
13
14@dataclasses.dataclass
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)                      
───────────────────────────────────────────────────
 subcommands ─────────────────────────────────────╮
 (default: cmd:checkout)                           
   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    binaries: bool = False
21
22# Possible optimizer configurations.
23
24@dataclasses.dataclass
25class Adam:
26    learning_rate: float = 1e-3
27    betas: tuple[float, float] = (0.9, 0.999)
28
29@dataclasses.dataclass
30class Sgd:
31    learning_rate: float = 3e-4
32
33# Train script.
34
35def train(
36    dataset: Mnist | ImageNet = Mnist(),
37    optimizer: Adam | Sgd = Adam(),
38) -> None:
39    """Example training script.
40
41    Args:
42        dataset: Dataset to train on.
43        optimizer: Optimizer to train with.
44
45    Returns:
46        None:
47    """
48    print(dataset)
49    print(optimizer)
50
51if __name__ == "__main__":
52    tyro.cli(train, config=(tyro.conf.CascadeSubcommandArgs,))

We apply the tyro.conf.CascadeSubcommandArgs flag. This allows arguments to be intermixed more flexibly:

$ python ./03_multiple_subcommands.py --help
usage: ./03_multiple_subcommands.py [-h] [OPTIONS] [DATASET] [OPTIMIZER]

Example training script.

 options ────────────────────────────────────────────────────────────────────╮
 -h, --help             show this help message and exit                       
──────────────────────────────────────────────────────────────────────────────
 dataset options ────────────────────────────────────────────────────────────╮
 (source subcommand: dataset:mnist)                                           
 ──────────────────────────────────────────────────────────────────────────── 
 --dataset.binary, --dataset.no-binary                                        
                        Set to load binary version of MNIST dataset.          
                        (default: False)                                      
──────────────────────────────────────────────────────────────────────────────
 optimizer options ──────────────────────────────────────────────────────────╮
 (source subcommand: optimizer:adam)                                          
 ──────────────────────────────────────────────────────────────────────────── 
 --optimizer.learning-rate FLOAT                                              
                        (default: 0.001)                                      
 --optimizer.betas FLOAT FLOAT                                                
                        (default: 0.9 0.999)                                  
──────────────────────────────────────────────────────────────────────────────
 subcommands ────────────────────────────────────────────────────────────────╮
 Dataset to train on. (default: dataset:mnist)                                
   dataset:mnist                                                            
   dataset:image-net                                                        
 ──────────────────────────────────────────────────────────────────────────── 
 Optimizer to train with. (default: optimizer:adam)                           
   optimizer:adam                                                           
   optimizer:sgd                                                            
──────────────────────────────────────────────────────────────────────────────
$ python ./03_multiple_subcommands.py dataset:mnist --help
usage: ./03_multiple_subcommands.py dataset:mnist [-h] [DATASET:MNIST OPTIONS] [{optimizer:adam,optimizer:sgd}]

 options ────────────────────────────────────────────────────────────────────╮
 -h, --help          show this help message and exit                          
──────────────────────────────────────────────────────────────────────────────
 dataset options ────────────────────────────────────────────────────────────╮
 --dataset.binary, --dataset.no-binary                                        
                     Set to load binary version of MNIST dataset. (default:   
                     False)                                                   
──────────────────────────────────────────────────────────────────────────────
 optimizer options ──────────────────────────────────────────────────────────╮

 (source subcommand: optimizer:adam)                                          
 ──────────────────────────────────────────────────────────────────────────── 
 --optimizer.learning-rate FLOAT                                              
                     (default: 0.001)                                         
 --optimizer.betas FLOAT FLOAT                                                
                     (default: 0.9 0.999)                                     
──────────────────────────────────────────────────────────────────────────────
 subcommands ────────────────────────────────────────────────────────────────╮
 Optimizer to train with. (default: optimizer:adam)                           
   optimizer:adam                                                           
   optimizer:sgd                                                            
──────────────────────────────────────────────────────────────────────────────
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --help
usage: ./03_multiple_subcommands.py optimizer:adam [-h] [OPTIMIZER:ADAM OPTIONS]

 options ─────────────────────────────────────────────────────────────────╮
 -h, --help  show this help message and exit                               
───────────────────────────────────────────────────────────────────────────
 dataset options ─────────────────────────────────────────────────────────╮
 --dataset.binary, --dataset.no-binary                                     
             Set to load binary version of MNIST dataset. (default: False) 
───────────────────────────────────────────────────────────────────────────
 optimizer options ───────────────────────────────────────────────────────╮
 --optimizer.learning-rate FLOAT                                           
             (default: 0.001)                                              
 --optimizer.betas FLOAT FLOAT                                             
             (default: 0.9 0.999)                                          
───────────────────────────────────────────────────────────────────────────
$ 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 ─────────────────────────────────╮
 (required)                                    
   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 ─────────────────────────────────╮
 (required)                                    
   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'

Nested subcommands

Unions over struct types can be nested to create hierarchical subcommand structures. By default, nested unions wrapped in Annotated are flattened into the parent union. To create a named subcommand group, annotate the nested union with tyro.conf.subcommand(name=...)().

 1# 06_nested_subcommands.py
 2from __future__ import annotations
 3
 4import dataclasses
 5
 6from typing_extensions import Annotated
 7
 8import tyro
 9
10@dataclasses.dataclass
11class Checkout:
12    """Checkout a branch."""
13
14    branch: str
15
16@dataclasses.dataclass
17class Commit:
18    """Commit changes."""
19
20    message: str
21
22@dataclasses.dataclass
23class Push:
24    """Push commits to a remote."""
25
26    remote: str = "origin"
27    branch: str = "main"
28
29@dataclasses.dataclass
30class Pull:
31    """Pull commits from a remote."""
32
33    remote: str = "origin"
34
35Remote = Annotated[
36    Push | Pull,
37    tyro.conf.subcommand(name="remote", description="Remote operations."),
38]
39
40if __name__ == "__main__":
41    cmd = tyro.cli(Checkout | Commit | Remote)
42    print(cmd)

Show top-level subcommands:

$ python ./06_nested_subcommands.py --help
usage: ./06_nested_subcommands.py [-h] {checkout,commit,remote}

 options ─────────────────────────────────────╮
 -h, --help    show this help message and exit 
───────────────────────────────────────────────
 subcommands ─────────────────────────────────╮
 (required)                                    
   checkout  Checkout a branch.              
   commit    Commit changes.                 
   remote    Remote operations.              
───────────────────────────────────────────────

The checkout subcommand:

$ python ./06_nested_subcommands.py checkout --branch main
Checkout(branch='main')

remote is a named group containing push and pull:

$ python ./06_nested_subcommands.py remote --help
usage: ./06_nested_subcommands.py remote [-h] {push,pull}

Remote operations.

 options ───────────────────────────────────╮
 -h, --help  show this help message and exit 
─────────────────────────────────────────────
 subcommands ───────────────────────────────╮
 (required)                                  
   push    Push commits to a remote.       
   pull    Pull commits from a remote.     
─────────────────────────────────────────────
$ python ./06_nested_subcommands.py remote push --remote origin --branch main
Push(remote='origin', branch='main')
$ python ./06_nested_subcommands.py remote pull --remote origin
Pull(remote='origin')