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)