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 dataset:mnist 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, inspired by Typer and
cyclopts.
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", aliases=["sum"])
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 (sum) │ │ 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
$ python 04_decorator_subcommands.py sum --a 5 --b 3 '#' via alias ╭─ Unrecognized options ────────────────────────────────────╮ │ Unrecognized options: #, via, alias │ │ ───────────────────────────────────────────────────────── │ │ For full helptext, run 04_decorator_subcommands.py --help │ ╰───────────────────────────────────────────────────────────╯
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')
Nested decorator-based subcommands¶
A SubcommandApp can be registered as a subcommand on another
SubcommandApp, producing a hierarchical CLI. Use aliases= for
short names and is_default=True to pick which subcommand runs when none
is named.
1# 07_decorator_nested.py
2from tyro.extras import SubcommandApp
3
4# Nested app for database commands.
5db = SubcommandApp()
6
7@db.command(aliases=["m"])
8def migrate(version: int = 1) -> None:
9 """Apply schema migrations."""
10 print(f"migrating to version {version}")
11
12@db.command
13def seed(rows: int = 10) -> None:
14 """Populate with seed data."""
15 print(f"seeding {rows} rows")
16
17# Top-level app.
18app = SubcommandApp()
19app.command(db, name="db")
20
21@app.command(is_default=True)
22def greet(name: str = "world") -> None:
23 """Default action when no subcommand is given."""
24 print(f"hello {name}")
25
26if __name__ == "__main__":
27 app.cli()
$ python 07_decorator_nested.py --help usage: 07_decorator_nested.py [-h] [{db,greet}] ╭─ options ────────────────────────────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─ subcommands ────────────────────────────────────────────────────────────────╮ │ (default: greet) │ │ • db This class provides a decorator-based API for subcommands in │ │ :mod:`tyro`, inspired by `Typer │ │ <https://typer.tiangolo.com/>`_ and │ │ `cyclopts <https://cyclopts.readthedocs.io/>`_. │ │ Under-the-hood, this is a light wrapper │ │ over :func:`tyro.cli`. │ │ │ │ │ │ Example: │ │ │ │ │ │ .. code-block:: python │ │ │ │ │ │ from tyro.extras import SubcommandApp │ │ │ │ │ │ app = SubcommandApp() │ │ │ │ │ │ @app.command │ │ def greet(name: str, loud: bool = False): │ │ '''Greet someone.''' │ │ greeting = f"Hello, {name}!" │ │ if loud: │ │ greeting = greeting.upper() │ │ print(greeting) │ │ │ │ │ │ @app.command(name="addition", aliases=["sum"]) │ │ def add(a: int, b: int): │ │ '''Add two numbers.''' │ │ print(f"{a} + {b} = {a + b}") │ │ │ │ │ │ if __name__ == "__main__": │ │ app.cli() │ │ │ │ │ │ Subcommand groups can be nested by registering one │ │ :class:`SubcommandApp` as a subcommand on another: │ │ │ │ │ │ .. code-block:: python │ │ │ │ │ │ db = SubcommandApp() │ │ │ │ │ │ @db.command │ │ def migrate(): ... │ │ │ │ │ │ @db.command │ │ def seed(): ... │ │ │ │ │ │ app = SubcommandApp() │ │ app.command(db, name="db") # `mycli db migrate`, `mycli │ │ db seed` │ │ │ │ │ │ To make one subcommand the default when no subcommand is │ │ given, pass │ │ ``is_default=True``: │ │ │ │ │ │ .. code-block:: python │ │ │ │ │ │ @app.command(is_default=True) │ │ def run(name: str = "world"): │ │ print(f"hello {name}") │ │ │ │ │ │ # `mycli` runs `run("world")`; `mycli --name alice` runs │ │ `run("alice")`; │ │ # `mycli run --name alice` also works. │ │ │ │ │ │ Usage: │ │ │ │ │ │ .. code-block:: bash │ │ │ │ │ │ python my_script.py greet Alice │ │ python my_script.py greet Bob --loud │ │ python my_script.py addition 5 3 │ │ python my_script.py sum 5 3 # via alias │ │ • greet Default action when no subcommand is given. │ ╰──────────────────────────────────────────────────────────────────────────────╯
$ python 07_decorator_nested.py db --help usage: 07_decorator_nested.py db [-h] {migrate,seed} This class provides a decorator-based API for subcommands in :mod:`tyro`, inspired by `Typer <https://typer.tiangolo.com/>`_ and `cyclopts <https://cyclopts.readthedocs.io/>`_. Under-the-hood, this is a light wrapper over :func:`tyro.cli`. Example: .. code-block:: python from tyro.extras import SubcommandApp app = SubcommandApp() @app.command def greet(name: str, loud: bool = False): '''Greet someone.''' greeting = f"Hello, {name}!" if loud: greeting = greeting.upper() print(greeting) @app.command(name="addition", aliases=["sum"]) def add(a: int, b: int): '''Add two numbers.''' print(f"{a} + {b} = {a + b}") if __name__ == "__main__": app.cli() Subcommand groups can be nested by registering one :class:`SubcommandApp` as a subcommand on another: .. code-block:: python db = SubcommandApp() @db.command def migrate(): ... @db.command def seed(): ... app = SubcommandApp() app.command(db, name="db") # `mycli db migrate`, `mycli db seed` To make one subcommand the default when no subcommand is given, pass ``is_default=True``: .. code-block:: python @app.command(is_default=True) def run(name: str = "world"): print(f"hello {name}") # `mycli` runs `run("world")`; `mycli --name alice` runs `run("alice")`; # `mycli run --name alice` also works. Usage: .. code-block:: bash python my_script.py greet Alice python my_script.py greet Bob --loud python my_script.py addition 5 3 python my_script.py sum 5 3 # via alias ╭─ options ────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰──────────────────────────────────────────────╯ ╭─ subcommands ────────────────────────────────╮ │ (required) │ │ • migrate (m) │ │ Apply schema migrations. │ │ • seed Populate with seed data. │ ╰──────────────────────────────────────────────╯
$ python 07_decorator_nested.py db migrate --version 7
migrating to version 7
$ python 07_decorator_nested.py db seed --rows 5
seeding 5 rows
$ python 07_decorator_nested.py greet --name Alice
hello Alice
$ python 07_decorator_nested.py '#' runs the is_default branch ╭─ Unrecognized options ─────────────────────────────────╮ │ Unrecognized options: #, runs, the, is_default, branch │ │ ────────────────────────────────────────────────────── │ │ Available subcommands: db, greet │ │ ────────────────────────────────────────────────────── │ │ For full helptext, run 07_decorator_nested.py --help │ ╰────────────────────────────────────────────────────────╯