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)))