Union Types: Subcommands and Configuration Selection
Union types enable powerful multi-way dispatching in your CLI programs, allowing users to choose between different configurations or implementations at runtime. This is essential for building flexible, composable CLI tools.
Why Union Types Matter
When building complex CLI applications, you often need to support multiple alternatives:
Different optimizer algorithms (Adam vs SGD vs RMSprop)
Different model architectures (ResNet vs Vision Transformer vs CNN)
Different data loaders or preprocessing strategies
Different deployment environments (local vs cloud vs GPU)
Without Union types, you'd need separate scripts or manual conditional logic. With Union types, params-proto automatically generates:
✅ Subcommand-like syntax that feels natural
✅ Isolated parameter spaces (each option has its own parameters)
✅ Type-safe dispatch with isinstance() checks
✅ Automatic help generation per option
Quick Reference
python
from dataclasses import dataclassfrom params_proto import proto# Pattern 1: Union of multiple classes (choose one)@dataclassclass Adam: lr: float = 0.001@dataclassclass SGD: lr: float = 0.001 momentum: float = 0.9@proto.clidef train(optimizer: Adam | SGD, epochs: int = 10): pass# CLI: python train.py adam --lr 0.01 --epochs 50# CLI: python train.py sgd --momentum 0.95
Note
Version 3.2.0+: Subcommand attributes are unprefixed by default.
Use --lr instead of --optimizer.lr. The prefixed syntax still works for backwards compatibility.
python
# Pattern 2: Single class parameter (configuration)@dataclassclass Model: name: str = "resnet50" hidden_size: int = 256@proto.clidef main(model: Model): pass# CLI: python main.py model --hidden-size 512# CLI: python main.py model --name vit
Attribute overrides (v3.2.0+: unprefixed by default):
python
@dataclassclass Config: batch_size: int = 32 # Python: snake_case learning_rate: float = 0.001# CLI uses kebab-case (unprefixed by default)--batch-size 64--learning-rate 0.01# Prefixed syntax still works--config.batch-size 64--config.learning-rate 0.01
Using @proto.prefix for Required Prefixes
If you want to require prefixed syntax for a class (e.g., for disambiguation), decorate it with @proto.prefix:
python
@proto.prefix # Now requires --config.batch-size syntax@dataclassclass Config: batch_size: int = 32@dataclassclass Model: batch_size: int = 16 # Same attr name, no conflict@proto.clidef train(config: Config, model: Model): pass# CLI: Config requires prefix, Model doesn't# python train.py config model --config.batch-size 64 --batch-size 32
Examples
Example 1: Optimizer Selection
Command-line usage:
bash
# Choose optimizer and override defaults (unprefixed)python train.py adam --lr 0.01python train.py sgd --momentum 0.95# Prefixed syntax also workspython train.py --optimizer:Adam --optimizer.beta1 0.95
Implementation:
python
@dataclassclass Adam: lr: float = 0.001 # Learning rate beta1: float = 0.9 # Exponential decay rate for 1st moment beta2: float = 0.999 # Exponential decay rate for 2nd moment@dataclassclass SGD: lr: float = 0.001 # Learning rate momentum: float = 0.9 # Momentum factor@proto.clidef train(optimizer: Adam | SGD): # Union type selector """Train with chosen optimizer.""" if isinstance(optimizer, Adam): print(f"Adam: lr={optimizer.lr}, beta1={optimizer.beta1}, beta2={optimizer.beta2}") elif isinstance(optimizer, SGD): print(f"SGD: lr={optimizer.lr}, momentum={optimizer.momentum}")
Example 2: Configuration Class
Command-line usage:
bash
# Positional class selection with unprefixed attrspython connect.py database-config --host prod.example.com --port 3306# Or named selection with prefixed attrspython connect.py --db:DatabaseConfig --db.user root
Implementation:
python
@dataclassclass DatabaseConfig: host: str = "localhost" # Database host port: int = 5432 # Database port user: str = "admin" # Database user@proto.clidef connect(db: DatabaseConfig): # Single class (still uses Union mechanism) """Connect to database with configuration.""" print(f"Connecting to {db.host}:{db.port} as {db.user}")
Example 3: Mixed Union and Regular Parameters
Command-line usage:
bash
# Union option + shared parameters (unprefixed)python render.py perspective-camera --fov 45 --output scene.png --verbose# Different union option with overridespython render.py orthographic-camera --scale 2.0 --verbose# Help shows all optionspython render.py --help
Implementation:
python
@dataclassclass PerspectiveCamera: fov: float = 60.0 # Field of view in degrees@dataclassclass OrthographicCamera: scale: float = 1.0 # Scale factor@proto.clidef render( camera: PerspectiveCamera | OrthographicCamera, # Union selector output: str = "render.png", # Shared parameter verbose: bool = False, # Shared flag): """Render scene with selected camera.""" if isinstance(camera, PerspectiveCamera): print(f"Perspective render: fov={camera.fov}, output={output}") else: print(f"Orthographic render: scale={camera.scale}, output={output}") if verbose: print("Verbose output enabled")
Key point: Union parameters are required, while other parameters can be optional with defaults.
Example 4: @proto.prefix Classes Require Prefixed Syntax
When a Union class is decorated with @proto.prefix, its CLI attributes require prefixed syntax: