Skip to content

Tyro

Tyro

Although omegaconf+argparse is great for a research project, it's not suitable for a python package CLI (we have no place to put all the config files).

With tyro, we can write a package CLI elegantly!

Function

The simplest use case, turn a function into a CLI.

import tyro

def main(
    x: int, # required
    path: str = 'logs', # with a default value
):
    print(x, path)

if __name__ == '__main__':
    tyro.cli(main)
python main.py --help
python main.py --x 1
python main.py --x 1 --path wow

Dataclass

For more complex configs.

A dataclass is a specialized object for holding static data:

from dataclasses import dataclass

@dataclass
class A:
    #x # without type specification it will throw error!
    x: str # ok
    y: int = 1 # with default value

#print(A.x) # error
print(A.y) # 1

a = A(x='x')
print(a.x, a.y) # 'x', 1

We can use tyro to parse a dataclass:

import tyro
from dataclasses import dataclass
from typing import Tuple, Literal, Union
import enum

class Color(enum.Enum):
    red = enum.auto()
    green = enum.auto()
    blue = enum.auto()

@dataclass
class Options:
    # NOTE: must place positional values > required vales > default values
    # NOTE: tyro will detect comments (after > above) as the help string.

    # positional value
    x: tyro.conf.Positional[int]

    # required value
    y: int 

    # required bool, "--flag1 True/False"
    flag1: bool 

    # default values
    path: str = 'logs' 

    # variable-length
    shape: Tuple[int, ...] = (64,)

    # multi-value
    size: Tuple[int, int] = (64, 64)

    # choice by enum
    color: Color = Color.red

    # choice by Literal
    color2: Literal['red', 'green', 'blue'] = 'red'

    # bool default to False, use "--flag2" to set True
    flag2: bool = False 

    # bool default to True, use "--no-flag3" to set False
    flag3: bool = True 

    # mixed type by union
    int_or_str: Union[int, str] = 0


if __name__ == '__main__'    :
    opt = tyro.cli(Options)
    print(opt)
python main.py --help

# output
usage: main.py [-h] --x INT --flag1 {True,False} [--path STR] [--shape INT [INT ...]] [--size INT INT]
               [--color {red,green,blue}] [--color2 {red,green,blue}] [--flag2] [--no-flag3]
               [--int-or-str INT|STR]

╭─ positional arguments ───────────────────────────────────────────────────────────────────────╮
│ INT                     positional parameters (required)                                     │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ arguments ──────────────────────────────────────────────────────────────────────────────────╮
│ -h, --help              show this help message and exit                                      │
│ --x INT                 required value (required)                                            │
│ --flag1 {True,False}    required bool, "--flag1 True/False" (required)                       │
│ --path STR              default values (default: logs)                                       │
│ --shape INT [INT ...]   variable-length (default: 64)                                        │
│ --size INT INT          multi-value (default: 64 64)                                         │
│ --color {red,green,blue}                                                                     │
│                         choice by enum (default: red)                                        │
│ --color2 {red,green,blue}                                                                    │
│                         choice by Literal (default: red)                                     │
│ --flag2                 bool default False, use "--flag2" to set True (sets: flag2=True)     │
│ --no-flag3              bool default True, use "--no-flag3" to set False (sets: flag3=False) │
│ --int-or-str INT|STR    mixed type by union (default: 0)                                     │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯

Hierarchical Configs

For nesting configs.

import tyro
from dataclasses import dataclass, field
from typing import Tuple, Literal, Union

@dataclass
class OptimizerOption:
    type: Literal['adam', 'sgd'] = 'adam'
    lr: float = 1e-3

@dataclass
class Options:
    # nested option: just declare the type
    # the field(...) is for python 3.11 compatibility, ref: https://github.com/NVIDIA/NeMo/issues/7166#issuecomment-1694251124
    optimizer: OptimizerOption = field(default_factory=lambda: OptimizerOption(lr=1e-2))
    # other options
    seed: int = 0
    iterations: int = 3000


if __name__ == '__main__'    :
    opt = tyro.cli(Options)
    print(opt)
python main.py --help

# output
usage: main.py [-h] [--optimizer.type {adam,sgd}] [--optimizer.lr FLOAT] [--seed INT] [--iterations INT]

╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --seed INT              other options (default: 0)      │
│ --iterations INT        other options (default: 3000)   │
╰─────────────────────────────────────────────────────────╯
╭─ optimizer arguments ───────────────────────────────────╮
│ nested option: just declare the type                    │
│ ────────────────────────────────────────                │
│ --optimizer.type {adam,sgd}                             │
│                         (default: adam)                 │
│ --optimizer.lr FLOAT    (default: 0.001)                │
╰─────────────────────────────────────────────────────────╯

Subcommands

import tyro
from dataclasses import dataclass
from typing import Tuple, Literal, Union

@dataclass
class Train:
    type: Literal['adam', 'sgd'] = 'adam'
    lr: float = 1e-3
    seed: int = 0
    iterations: int = 3000

@dataclass
class Test:
    # other options
    resolution: int = 1024


if __name__ == '__main__':
    # pass in a Union of all configs of subcommands
    opt = tyro.cli(Union[Train, Test])
    print(opt) # you'll have to decide whether opt is Train or Test by isinstance...
python main.py --help

# output
usage: main.py [-h] {train,test}

╭─ arguments ─────────────────────────────────────────╮
│ -h, --help          show this help message and exit │
╰─────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────╮
│ {train,test}                                        │
│     train                                           │
│     test                                            │
╰─────────────────────────────────────────────────────╯

python main.py train --help

# output
usage: main.py train [-h] [--type {adam,sgd}] [--lr FLOAT] [--seed INT] [--iterations INT]

╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --type {adam,sgd}       (default: adam)                 │
│ --lr FLOAT              (default: 0.001)                │
│ --seed INT              (default: 0)                    │
│ --iterations INT        (default: 3000)                 │
╰─────────────────────────────────────────────────────────╯

Subcommands for overriding default configs

This can reach the same effect as using multiple config files with omegaconf, but it's a little tricky...

import tyro
from dataclasses import dataclass
from typing import Tuple, Literal, Union, Dict

@dataclass
class Options:
    name: str
    type: Literal['adam', 'sgd'] = 'adam'
    lr: float = 1e-3
    seed: int = 0
    iterations: int = 3000

# different default configs
config_defaults: Dict[str, Options] = {}
config_descriptions: Dict[str, str] = {}

config_defaults['A'] = Options(name='A')
config_descriptions['A'] = 'this is setting A'

config_defaults['B'] = Options(name='B', lr=1e-4) # different default lr
config_descriptions['B'] = 'this is setting B'

if __name__ == '__main__':
    opt = tyro.cli(tyro.extras.subcommand_type_from_defaults(config_defaults, config_descriptions))
    print(opt)
python main.py --help

# output
usage: main.py [-h] {A,B}

╭─ arguments ───────────────────────────────────────╮
│ -h, --help        show this help message and exit │
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
│ {A,B}                                             │
│     A                 this is setting A           │
│     B                 this is setting B           │
╰───────────────────────────────────────────────────╯

python main.py A --help

# output
usage: tmp_tyro.py A [-h] [--name STR] [--type {adam,sgd}] [--lr FLOAT] [--seed INT] [--iterations INT]

this is setting A

╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --name STR              (default: A)                    │
│ --type {adam,sgd}       (default: adam)                 │
│ --lr FLOAT              (default: 0.001)                │
│ --seed INT              (default: 0)                    │
│ --iterations INT        (default: 3000)                 │
╰─────────────────────────────────────────────────────────╯