Hierarchical Structures¶
In these examples, we show how tyro.cli()
can be used to instantiate
hierarchical structures. This can enable modular, reusable, and composable CLI
interfaces.
Nested Dataclasses¶
Structures (typically dataclasses.dataclass()
) can be nested to build hierarchical configuration
objects. This helps with modularity and grouping in larger projects.
1# 01_nesting.py
2import dataclasses
3
4import tyro
5
6@dataclasses.dataclass
7class OptimizerConfig:
8 learning_rate: float = 3e-4
9 weight_decay: float = 1e-2
10
11@dataclasses.dataclass
12class Config:
13 # Optimizer options.
14 opt: OptimizerConfig
15
16 # Random seed.
17 seed: int = 0
18
19if __name__ == "__main__":
20 config = tyro.cli(Config)
21 print(dataclasses.asdict(config))
$ python ./01_nesting.py --help usage: 01_nesting.py [-h] [--seed INT] [--opt.learning-rate FLOAT] [--opt.weight-decay FLOAT] ╭─ options ─────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --seed INT Random seed. (default: 0) │ ╰───────────────────────────────────────────────────╯ ╭─ opt options ─────────────────────────────────────╮ │ Optimizer options. │ │ ─────────────────────────────────── │ │ --opt.learning-rate FLOAT │ │ (default: 0.0003) │ │ --opt.weight-decay FLOAT │ │ (default: 0.01) │ ╰───────────────────────────────────────────────────╯
$ python ./01_nesting.py --opt.learning-rate 1e-3
{'opt': {'learning_rate': 0.001, 'weight_decay': 0.01}, 'seed': 0}
$ python ./01_nesting.py --seed 4
{'opt': {'learning_rate': 0.0003, 'weight_decay': 0.01}, 'seed': 4}
Structures as Function Arguments¶
Structures can be used as input to functions.
1# 02_nesting_in_func.py
2import dataclasses
3import pathlib
4
5import tyro
6
7@dataclasses.dataclass
8class OptimizerConfig:
9 learning_rate: float = 3e-4
10 weight_decay: float = 1e-2
11
12@dataclasses.dataclass
13class Config:
14 # Optimizer options.
15 optimizer: OptimizerConfig
16
17 # Random seed.
18 seed: int = 0
19
20def train(
21 out_dir: pathlib.Path,
22 config: Config,
23) -> None:
24 """Train a model.
25
26 Args:
27 out_dir: Where to save logs and checkpoints.
28 config: Experiment configuration.
29 """
30 print(f"Saving to: {out_dir}")
31 print(f"Config: {config}")
32
33if __name__ == "__main__":
34 tyro.cli(train)
$ python ./02_nesting_in_func.py --help usage: 02_nesting_in_func.py [-h] [OPTIONS] Train a model. ╭─ options ──────────────────────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --out-dir PATH Where to save logs and checkpoints. (required) │ ╰────────────────────────────────────────────────────────────────────────╯ ╭─ config options ───────────────────────────────────────────────────────╮ │ Experiment configuration. │ │ ───────────────────────────────────────────────── │ │ --config.seed INT Random seed. (default: 0) │ ╰────────────────────────────────────────────────────────────────────────╯ ╭─ config.optimizer options ─────────────────────────────────────────────╮ │ Optimizer options. │ │ ───────────────────────────────────────────────── │ │ --config.optimizer.learning-rate FLOAT │ │ (default: 0.0003) │ │ --config.optimizer.weight-decay FLOAT │ │ (default: 0.01) │ ╰────────────────────────────────────────────────────────────────────────╯
$ python ./02_nesting_in_func.py --out-dir /tmp/test1
Saving to: /tmp/test1
Config: Config(optimizer=OptimizerConfig(learning_rate=0.0003, weight_decay=0.01), seed=0)
$ python ./02_nesting_in_func.py --out-dir /tmp/test2 --config.seed 4
Saving to: /tmp/test2
Config: Config(optimizer=OptimizerConfig(learning_rate=0.0003, weight_decay=0.01), seed=4)
Nesting in Containers¶
Structures can be nested inside of standard containers.
Warning
When placing structures inside of containers like lists or tuples, the length of the container must be inferrable from the annotation or default value.
1# 03_nesting_containers.py
2import dataclasses
3
4import tyro
5
6@dataclasses.dataclass
7class RGB:
8 r: int
9 g: int
10 b: int
11
12@dataclasses.dataclass
13class Args:
14 color_tuple: tuple[RGB, RGB]
15 color_dict: dict[str, RGB] = dataclasses.field(
16 # We can't use mutable values as defaults directly.
17 default_factory=lambda: {
18 "red": RGB(255, 0, 0),
19 "green": RGB(0, 255, 0),
20 "blue": RGB(0, 0, 255),
21 }
22 )
23
24if __name__ == "__main__":
25 args = tyro.cli(Args)
26 print(args)
$ python ./03_nesting_containers.py --help usage: 03_nesting_containers.py [-h] [OPTIONS] ╭─ options ───────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰─────────────────────────────────────────────────────────╯ ╭─ color-tuple.0 options ─────────────────────────────────╮ │ --color-tuple.0.r INT (required) │ │ --color-tuple.0.g INT (required) │ │ --color-tuple.0.b INT (required) │ ╰─────────────────────────────────────────────────────────╯ ╭─ color-tuple.1 options ─────────────────────────────────╮ │ --color-tuple.1.r INT (required) │ │ --color-tuple.1.g INT (required) │ │ --color-tuple.1.b INT (required) │ ╰─────────────────────────────────────────────────────────╯ ╭─ color-dict.red options ────────────────────────────────╮ │ --color-dict.red.r INT (default: 255) │ │ --color-dict.red.g INT (default: 0) │ │ --color-dict.red.b INT (default: 0) │ ╰─────────────────────────────────────────────────────────╯ ╭─ color-dict.green options ──────────────────────────────╮ │ --color-dict.green.r INT │ │ (default: 0) │ │ --color-dict.green.g INT │ │ (default: 255) │ │ --color-dict.green.b INT │ │ (default: 0) │ ╰─────────────────────────────────────────────────────────╯ ╭─ color-dict.blue options ───────────────────────────────╮ │ --color-dict.blue.r INT │ │ (default: 0) │ │ --color-dict.blue.g INT │ │ (default: 0) │ │ --color-dict.blue.b INT │ │ (default: 255) │ ╰─────────────────────────────────────────────────────────╯
Dictionaries and TypedDict¶
Dictionary inputs can be specified using either a standard dict[K, V]
annotation, or a TypedDict
subclass.
For configuring TypedDict
, we also support total={True/False}
,
typing.Required
, and typing.NotRequired
. See the Python docs for all TypedDict
features.
1# 04_dictionaries.py
2from typing import TypedDict
3
4from typing_extensions import NotRequired
5
6import tyro
7
8class DictionarySchemaA(
9 TypedDict,
10 # Setting `total=False` specifies that not all keys need to exist.
11 total=False,
12):
13 learning_rate: float
14 betas: tuple[float, float]
15
16class DictionarySchemaB(TypedDict):
17 learning_rate: NotRequired[float]
18 """NotRequired[] specifies that a particular key doesn't need to exist."""
19 betas: tuple[float, float]
20
21def main(
22 typed_dict_a: DictionarySchemaA,
23 typed_dict_b: DictionarySchemaB,
24 standard_dict: dict[str, float] = {
25 "learning_rate": 3e-4,
26 "beta1": 0.9,
27 "beta2": 0.999,
28 },
29) -> None:
30 assert isinstance(typed_dict_a, dict)
31 assert isinstance(typed_dict_b, dict)
32 assert isinstance(standard_dict, dict)
33 print("Typed dict A:", typed_dict_a)
34 print("Typed dict B:", typed_dict_b)
35 print("Standard dict:", standard_dict)
36
37if __name__ == "__main__":
38 tyro.cli(main)
$ python ./04_dictionaries.py --help usage: 04_dictionaries.py [-h] [OPTIONS] ╭─ options ──────────────────────────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─ typed-dict-a options ─────────────────────────────────────────────────────╮ │ --typed-dict-a.learning-rate FLOAT │ │ (unset by default) │ │ --typed-dict-a.betas FLOAT FLOAT │ │ (unset by default) │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─ typed-dict-b options ─────────────────────────────────────────────────────╮ │ --typed-dict-b.learning-rate FLOAT │ │ NotRequired[] specifies that a particular key doesn't │ │ need to exist. (unset by default) │ │ --typed-dict-b.betas FLOAT FLOAT │ │ (required) │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─ standard-dict options ────────────────────────────────────────────────────╮ │ --standard-dict.learning-rate FLOAT │ │ (default: 0.0003) │ │ --standard-dict.beta1 FLOAT │ │ (default: 0.9) │ │ --standard-dict.beta2 FLOAT │ │ (default: 0.999) │ ╰────────────────────────────────────────────────────────────────────────────╯
$ python ./04_dictionaries.py --typed-dict-a.learning-rate 3e-4 --typed-dict-b.betas 0.9 0.999
Typed dict A: {'learning_rate': 0.0003}
Typed dict B: {'betas': (0.9, 0.999)}
Standard dict: {'learning_rate': 0.0003, 'beta1': 0.9, 'beta2': 0.999}
$ python ./04_dictionaries.py --typed-dict-b.betas 0.9 0.999
Typed dict A: {}
Typed dict B: {'betas': (0.9, 0.999)}
Standard dict: {'learning_rate': 0.0003, 'beta1': 0.9, 'beta2': 0.999}
Tuples and NamedTuple¶
Example using tyro.cli()
to instantiate tuple types. tuple
,
typing.Tuple
, and typing.NamedTuple
are all supported.
1# 05_tuples.py
2from typing import NamedTuple
3
4import tyro
5
6# Named tuples are interpreted as nested structures.
7class Color(NamedTuple):
8 r: int
9 g: int
10 b: int
11
12class TupleType(NamedTuple):
13 """Description.
14 This should show up in the helptext!"""
15
16 # Tuple types can contain raw values.
17 color: tuple[int, int, int] = (255, 0, 0)
18
19 # Tuple types can contain nested structures.
20 two_colors: tuple[Color, Color] = (Color(255, 0, 0), Color(0, 255, 0))
21
22if __name__ == "__main__":
23 x = tyro.cli(TupleType)
24 assert isinstance(x, tuple)
25 print(x)
$ python ./05_tuples.py --help usage: 05_tuples.py [-h] [OPTIONS] Description. This should show up in the helptext! ╭─ options ──────────────────────────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --color INT INT INT Tuple types can contain raw values. (default: 255 │ │ 0 0) │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─ two-colors.0 options ─────────────────────────────────────────────────────╮ │ --two-colors.0.r INT (default: 255) │ │ --two-colors.0.g INT (default: 0) │ │ --two-colors.0.b INT (default: 0) │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─ two-colors.1 options ─────────────────────────────────────────────────────╮ │ --two-colors.1.r INT (default: 0) │ │ --two-colors.1.g INT (default: 255) │ │ --two-colors.1.b INT (default: 0) │ ╰────────────────────────────────────────────────────────────────────────────╯
$ python ./05_tuples.py --color 127 127 127
TupleType(color=(127, 127, 127), two_colors=(Color(r=255, g=0, b=0), Color(r=0, g=255, b=0)))
$ python ./05_tuples.py --two-colors.1.r 127 --two-colors.1.g 0 --two-colors.1.b 0
TupleType(color=(255, 0, 0), two_colors=(Color(r=255, g=0, b=0), Color(r=127, g=0, b=0)))
Pydantic Integration¶
In addition to standard dataclasses, tyro.cli()
also supports
Pydantic models.
1# 06_pydantic.py
2from pydantic import BaseModel, Field
3
4import tyro
5
6class Args(BaseModel):
7 """Description.
8 This should show up in the helptext!"""
9
10 field1: str
11 field2: int = Field(3, description="An integer field.")
12
13if __name__ == "__main__":
14 args = tyro.cli(Args)
15 print(args)
$ python ./06_pydantic.py --help usage: 06_pydantic.py [-h] --field1 STR [--field2 INT] Description. This should show up in the helptext! ╭─ options ───────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --field1 STR (required) │ │ --field2 INT An integer field. (default: 3) │ ╰─────────────────────────────────────────────────────╯
$ python ./06_pydantic.py --field1 hello
field1='hello' field2=3
$ python ./06_pydantic.py --field1 hello --field2 5
field1='hello' field2=5
Attrs Integration¶
In addition to standard dataclasses, tyro.cli()
also supports
attrs classes.
1# 07_attrs.py
2import attr
3
4import tyro
5
6@attr.s
7class Args:
8 """Description.
9 This should show up in the helptext!"""
10
11 field1: str = attr.ib()
12 """A string field."""
13
14 field2: int = attr.ib(factory=lambda: 5)
15 """A required integer field."""
16
17if __name__ == "__main__":
18 args = tyro.cli(Args)
19 print(args)
$ python ./07_attrs.py --help usage: 07_attrs.py [-h] --field1 STR [--field2 INT] Description. This should show up in the helptext! ╭─ options ──────────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --field1 STR A string field. (required) │ │ --field2 INT A required integer field. (default: 5) │ ╰────────────────────────────────────────────────────────────╯
$ python ./07_attrs.py --field1 hello
Args(field1='hello', field2=5)
$ python ./07_attrs.py --field1 hello --field2 5
Args(field1='hello', field2=5)