Generics

tyro’s understanding of Python’s type system includes user-defined parameterized types, which can reduce boilerplate and improve type safety.

Generics (3.12+)

This example uses syntax introduced in Python 3.12 (PEP 695).

Note

If used in conjunction with from __future__ import annotations, the updated type parameter syntax requires Python 3.12.4 or newer. For technical details, see this CPython PR.

 1# 01_generics_py312.py
 2import dataclasses
 3
 4import tyro
 5
 6@dataclasses.dataclass
 7class Point3[ScalarType: (int, float)]:
 8    x: ScalarType
 9    y: ScalarType
10    z: ScalarType
11    frame_id: str
12
13@dataclasses.dataclass
14class Triangle:
15    a: Point3[float]
16    b: Point3[float]
17    c: Point3[float]
18
19@dataclasses.dataclass
20class Args[ShapeType]:
21    shape: ShapeType
22
23if __name__ == "__main__":
24    args = tyro.cli(Args[Triangle])
25    print(args)
$ python ./01_generics_py312.py --help
usage: 01_generics_py312.py [-h] [OPTIONS]

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
╰─────────────────────────────────────────────────────────╯
╭─ shape.a options ───────────────────────────────────────╮
 --shape.a.x FLOAT       (required)                      
 --shape.a.y FLOAT       (required)                      
 --shape.a.z FLOAT       (required)                      
 --shape.a.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯
╭─ shape.b options ───────────────────────────────────────╮
 --shape.b.x FLOAT       (required)                      
 --shape.b.y FLOAT       (required)                      
 --shape.b.z FLOAT       (required)                      
 --shape.b.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯
╭─ shape.c options ───────────────────────────────────────╮
 --shape.c.x FLOAT       (required)                      
 --shape.c.y FLOAT       (required)                      
 --shape.c.z FLOAT       (required)                      
 --shape.c.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯

Generics (Python <3.12)

The legacy typing.Generic and typing.TypeVar syntax for generic types is also supported.

 1# 02_generics.py
 2import dataclasses
 3from typing import Generic, TypeVar
 4
 5import tyro
 6
 7ScalarType = TypeVar("ScalarType", int, float)
 8ShapeType = TypeVar("ShapeType")
 9
10@dataclasses.dataclass
11class Point3(Generic[ScalarType]):
12    x: ScalarType
13    y: ScalarType
14    z: ScalarType
15    frame_id: str
16
17@dataclasses.dataclass
18class Triangle:
19    a: Point3[float]
20    b: Point3[float]
21    c: Point3[float]
22
23@dataclasses.dataclass
24class Args(Generic[ShapeType]):
25    shape: ShapeType
26
27if __name__ == "__main__":
28    args = tyro.cli(Args[Triangle])
29    print(args)
$ python ./02_generics.py --help
usage: 02_generics.py [-h] [OPTIONS]

╭─ options ───────────────────────────────────────────────╮
 -h, --help              show this help message and exit 
╰─────────────────────────────────────────────────────────╯
╭─ shape.a options ───────────────────────────────────────╮
 --shape.a.x FLOAT       (required)                      
 --shape.a.y FLOAT       (required)                      
 --shape.a.z FLOAT       (required)                      
 --shape.a.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯
╭─ shape.b options ───────────────────────────────────────╮
 --shape.b.x FLOAT       (required)                      
 --shape.b.y FLOAT       (required)                      
 --shape.b.z FLOAT       (required)                      
 --shape.b.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯
╭─ shape.c options ───────────────────────────────────────╮
 --shape.c.x FLOAT       (required)                      
 --shape.c.y FLOAT       (required)                      
 --shape.c.z FLOAT       (required)                      
 --shape.c.frame-id STR  (required)                      
╰─────────────────────────────────────────────────────────╯

Generic Subcommands

Just like standard classes, generic classes within unions can be selected between using subcommands.

 1# 03_generic_subcommands.py
 2import dataclasses
 3from pathlib import Path
 4
 5import tyro
 6
 7@dataclasses.dataclass
 8class Sgd:
 9    lr: float = 1e-4
10
11@dataclasses.dataclass
12class Adam:
13    lr: float = 3e-4
14    betas: tuple[float, float] = (0.9, 0.999)
15
16@dataclasses.dataclass
17class Experiment[OptimizerT: (Adam, Sgd)]:
18    path: Path
19    opt: OptimizerT
20
21if __name__ == "__main__":
22    args = tyro.cli(Experiment[Adam] | Experiment[Sgd])
23    print(args)
$ python ./03_generic_subcommands.py --help
usage: 03_generic_subcommands.py [-h] {experiment-adam,experiment-sgd}

╭─ options ─────────────────────────────────────────╮
 -h, --help        show this help message and exit 
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
 {experiment-adam,experiment-sgd}                  
     experiment-adam                               
     experiment-sgd                                
╰───────────────────────────────────────────────────╯
$ python ./03_generic_subcommands.py experiment-adam --help
usage: 03_generic_subcommands.py experiment-adam [-h] --path PATH
                                                 [--opt.lr FLOAT]
                                                 [--opt.betas FLOAT FLOAT]

╭─ options ─────────────────────────────────────────────╮
 -h, --help            show this help message and exit 
 --path PATH           (required)                      
╰───────────────────────────────────────────────────────╯
╭─ opt options ─────────────────────────────────────────╮
 --opt.lr FLOAT        (default: 0.0003)               
 --opt.betas FLOAT FLOAT                               
                       (default: 0.9 0.999)            
╰───────────────────────────────────────────────────────╯
$ python ./03_generic_subcommands.py experiment-sgd --help
usage: 03_generic_subcommands.py experiment-sgd [-h] --path PATH
                                                [--opt.lr FLOAT]

╭─ options ─────────────────────────────────────────────╮
 -h, --help            show this help message and exit 
 --path PATH           (required)                      
╰───────────────────────────────────────────────────────╯
╭─ opt options ─────────────────────────────────────────╮
 --opt.lr FLOAT        (default: 0.0001)               
╰───────────────────────────────────────────────────────╯
$ python ./03_generic_subcommands.py experiment-adam --path /tmp --opt.lr 1e-3
Experiment(path=PosixPath('/tmp'), opt=Adam(lr=0.001, betas=(0.9, 0.999)))