Basic Series 5 - Configuration with Tyro
Code Style Note: The demonstration code examples in this post are intentionally compact for readability. In production code, you should follow PEP 8 style guidelines with proper spacing, line breaks, and formatting.
Why Use Configuration Tools Anyway?
- Single source of truth: No more copy-pasting option definitions across your code, CLI parser, and docs.
- Less boilerplate: Automatically handle parsing, defaults, validation, and help text without endless manual work.
Why Tyro?
- Pure Python: Just use type hints and dataclasses — no extra weird config files or DSLs to learn.
- One-liner CLI:
tyro.cli(...)
magically figures out flags, defaults, help messages, and even subcommands for you. - Better developer experience: You get IDE autocomplete, type checking, and neat, hierarchical interfaces. (Honestly, this is my favorite part!)
How to Use Tyro
The magic happens with tyro.cli()
, which can take either a function or a dataclass.
Option 1: Function (Quick and Simple)
Great for smaller scripts! Tyro will generate CLI flags from your function’s parameters and then call your function directly.
import tyro
def main(input: str, verbose: bool = False) -> None:
print(f"Input: {input}, verbose={verbose}")
if __name__ == "__main__":
tyro.cli(main)
When you run:
python script.py --help
You'll see something like:
usage: script.py [-h] --input STR [--verbose | --no-verbose]
╭─ options ───────────────────────────────╮
│ -h, --help show help message │
│ --input STR (required) │
│ --verbose, --no-verbose (default: False)│
╰─────────────────────────────────────────╯
Option 2: Dataclass (More Structured)
Perfect if your project has more settings and you want things tidy.
from dataclasses import dataclass
import tyro
# `dataclass` is needed here
@dataclass
class Config:
input_path: str
batch_size: int = 32
if __name__ == "__main__":
cfg = tyro.cli(Config)
print(f"Running with config: {cfg}")
Now Tyro will parse --input-path
and --batch-size
flags, then give you a Config
instance ready to use.
Using Default Configs & Overriding Them
Overriding Defaults in Dataclasses
You can pre-define a dataclass with certain default values and make Tyro only require what’s missing. Use the default=
argument:
from dataclasses import dataclass
import tyro
@dataclass
class Args:
string: str
reps: int = 3
if __name__ == "__main__":
args = tyro.cli(
Args,
default=Args(string="hello", reps=tyro.MISSING),
)
print(args)
- Here,
--string
defaults to"hello"
. reps
is marked as required (MISSING
), so you must specify it via--reps
.
Hierarchical Dataclasses
Want to manage more complex configs? You can nest dataclasses and keep things clean and organized.
from dataclasses import dataclass
import tyro
@dataclass
class OptimizerConfig:
learning_rate: float = 3e-4
weight_decay: float = 1e-2
@dataclass
class Config:
optimizer: OptimizerConfig
save_dir: str = "logs"
seed: int = 0
def train(config: Config) -> None:
# Your training logic here
pass
if __name__ == "__main__":
config = tyro.cli(Config)
train(config)
print(config)
Help Message Example
$ python script.py --help
usage: script.py [-h] [OPTIONS]
╭─ options ─────────────────────────────╮
│ -h, --help show help │
│ --save-dir STR (default: logs) │
│ --seed INT (default: 0) │
╰───────────────────────────────────────╯
╭─ optimizer options ───────────────────╮
│ --optimizer.learning-rate FLOAT │
│ (default: 0.0003)│
│ --optimizer.weight-decay FLOAT │
│ (default: 0.01) │
╰───────────────────────────────────────╯
Usage Examples
# Using all defaults
$ python script.py
Config(optimizer=OptimizerConfig(learning_rate=0.0003, weight_decay=0.01), save_dir='logs', seed=0)
# Overwriting some values
$ python script.py --save_dir runs --seed 1234 --optimizer.learning-rate 1e-5
Config(optimizer=OptimizerConfig(learning_rate=1e-05, weight_decay=0.01), save_dir='runs', seed=1234)
Saving Your Config
Saving configs is super helpful if you want to share experiments or reproduce results later. You can save them as JSON or YAML.
from dataclasses import dataclass
import os
import tyro
import yaml
@dataclass
class OptimizerConfig:
learning_rate: float = 3e-4
weight_decay: float = 1e-2
@dataclass
class Config:
optimizer: OptimizerConfig
save_dir: str = "logs"
seed: int = 0
def train(config: Config) -> None:
# Your training logic here
pass
if __name__ == "__main__":
config = tyro.cli(Config)
train(config)
os.makedirs(config.save_dir, exist_ok=True)
with open(os.path.join(config.save_dir, "config.yaml"), "w") as f:
yaml.safe_dump(dataclasses.asdict(config), f)
Other Alternatives
If you need something even fancier (like managing tons of nested configs or big experiments), check out Hydra. It’s built by Meta and great for complex projects.
Conclusion
Tyro is a super handy way to build clean, type-safe, and user-friendly CLIs without drowning in boilerplate code. Whether you're working on a quick experiment or a big research project, it helps you stay organized, reduce repetitive code, and make your scripts easier for others (and future you) to run.
If you want even more advanced features, nested configs, or extra tricks, be sure to dive into the official Tyro docs — there’s a lot more to explore.