params-proto v3 supports rich type annotations for parameters, providing type safety and automatic CLI help generation.
Known Type System Issues
✅ All major types are now fully supported in v3.0.0-rc24!
Previously broken types now working:
Literal[...] - Full validation of allowed values
Enum - Conversion to enum members (case-insensitive matching)
Path - Automatic instantiation via pathlib.Path()
dict - Safe parsing using ast.literal_eval
Required Parameters and Callable Types
Key Design Principle: For required parameters (those without default values), params-proto always calls the type hint as a constructor.
This is the foundation for Union types and potential subcommand support.
How It Works
When a parameter has no default value, its type hint must be callable:
python
from dataclasses import dataclassfrom params_proto import proto@dataclassclass Params: lr: float = 0.001 batch_size: int = 32@proto.clidef train( config: Params, # Required parameter - Params will be called as Params() epochs: int = 100, # Optional parameter with default): """Train with configuration.""" print(f"Using lr={config.lr}, batch_size={config.batch_size}")
This pattern enables Union types to act like subcommands:
python
from dataclasses import dataclass@dataclassclass Perspect: """Perspective camera configuration.""" fov: float = 60.0 # Field of view in degrees near: float = 0.1 # Near clipping plane far: float = 100.0 # Far clipping plane@dataclassclass Orthographic: """Orthographic camera configuration.""" zoom: float = 1.0 # Zoom level near: float = 0.1 # Near clipping plane far: float = 100.0 # Far clipping plane@proto.clidef render( camera: Perspect | Orthographic, # Required - user must choose which type output: str = "output.png",): """Render scene with camera configuration.""" print(f"Using camera: {camera}")
CLI Usage:
bash
# Choose perspective camera (calls Perspect())python render.py perspect --fov 45.0 --near 0.1# Choose orthographic camera (calls Orthographic())python render.py orthographic --zoom 2.0 --near 0.5# Get help for specific camera typepython render.py perspect --helppython render.py orthographic --help
The first positional argument selects which type to instantiate, and subsequent arguments configure that instance.
Why This Matters
This design enables:
Type-safe subcommand patterns without special subcommand syntax
Polymorphic configurations - choose between different config types at runtime
Composable types - any callable (dataclass, class, function) works as a type hint
Automatic CLI generation - params-proto generates appropriate help text for each union member
Required vs Optional
The callable type instantiation only applies to required parameters:
python
@proto.clidef train( # Required: Type hint MUST be callable, will be instantiated config: Params, # Optional: Uses the default value, type hint is for validation epochs: int = 100, lr: float = 0.001,): pass
For optional parameters (with defaults), the type hint is used for type conversion and validation, not instantiation.
Class Name to CLI Command Conversion
Important: When using Union types, class names are converted to lowercase CLI commands:
# Class names become kebab-case commandspython render.py perspect --fov 45.0 # Not Perspect or PERSPECTpython render.py orthographic --zoom 2.0 # Not Orthographic
from typing import Optional@proto.clidef process( config_file: Optional[str] = None, max_items: Optional[int] = None,): """Same as above, alternative syntax.""" pass
Union Types
Multiple Accepted Types
python
@proto.clidef train( lr: int | float = 0.001, # Accepts either int or float seed: int | None = None, # Accepts int or None): """Training with union types.""" pass
# Valid values are acceptedpython train.py --optimizer sgd --device cpu# Invalid values are rejected with clear errorpython train.py --optimizer invalid# error: value must be one of ('adam', 'sgd', 'rmsprop'), got 'invalid'# Numeric literals work toopython train.py --precision 16
--files [STR] List of input files (default: ['input.txt'])--dimensions [INT] Dimensions to process (default: [128, 256])--ratios [FLOAT] Aspect ratios (default: [0.5, 0.3])
How it works:
Arguments after --flag are collected until the next flag or end of arguments
Each value is converted to the element type (e.g., "256" → 256 for List[int])
The result is always a list, even with a single value
Tuple Types
Tuple types allow collecting multiple values from the CLI with automatic element type conversion. Both variable-length and fixed-size tuples are supported.
python
from typing import Tuple@proto.clidef train( # Variable-length tuple: Tuple[T, ...] collects values into a tuple learning_schedule: Tuple[float, ...] = (0.1, 0.01), # Fixed-size tuple: Tuple[T1, T2, T3] has specific type for each position image_size: Tuple[int, int] = (224, 224), # Mixed types in fixed-size tuple config: Tuple[int, str, float] = (10, "default", 0.5),): """Training with tuple types.""" print(f"Learning schedule: {learning_schedule}") # e.g., (0.5, 0.1, 0.01) print(f"Image size: {image_size[0]}x{image_size[1]}") # e.g., 256x256 print(f"Config: {config}") # e.g., (42, 'custom', 0.75)
CLI usage:
bash
# Variable-length tuple - collects all valuespython train.py --learning-schedule 0.5 0.1 0.01# Fixed-size tuple with specific position typespython train.py --image-size 256 256# Mixed type fixed-size tuplepython train.py --config 42 custom 0.75# Combine with other argumentspython train.py --learning-schedule 0.2 0.02 --image-size 512 512
Help text notation:
Variable-length: --param (INT,...)
Fixed-size: --param (INT,STR,FLOAT)
How it works:
Arguments after --flag are collected until the next flag or end of arguments
Each value is converted to its corresponding type position
For Tuple[T, ...], all values get converted to element type T
For fixed-size tuples, each value gets the type from its position
Path Types
pathlib.Path objects are automatically instantiated from string arguments.
python
from pathlib import Pathfrom params_proto import proto@proto.clidef process( input_dir: Path = Path("./data"), # Directory path output_file: Path = Path("output.txt"), # File path config: Path = Path.home() / ".config", # Home-relative path): """Process files with Path types.""" print(f"Reading from: {input_dir}") print(f"Writing to: {output_file}") print(f"Config at: {config}") # Paths are ready to use with pathlib methods input_dir.mkdir(parents=True, exist_ok=True)
@proto.clidef train(lr: float = 0.001): """Training function.""" # Type already converted by params-proto assert isinstance(lr, float), "lr must be float" # Add value validation if lr <= 0 or lr >= 1: raise ValueError("lr must be in (0, 1)")
Using Dataclass Validation
python
from dataclasses import dataclassfrom params_proto import proto@proto@dataclassclass Params: lr: float = 0.001 batch_size: int = 32 def __post_init__(self): """Validate after initialization.""" if self.lr <= 0: raise ValueError("lr must be positive") if self.batch_size < 1: raise ValueError("batch_size must be >= 1")
Type Hints Best Practices
1. Be Specific
python
# ✓ Good: specific literal@proto.clidef train(optimizer: Literal["adam", "sgd"] = "adam"): pass# ✗ Avoid: too general@proto.clidef train(optimizer: str = "adam"): pass
Type conversion also works with environment variables:
python
from params_proto import proto, EnvVar@proto.clidef train( # Environment variables are type-converted lr: float = EnvVar @ "LEARNING_RATE" | 0.001, batch_size: int = EnvVar @ "BATCH_SIZE" | 32, use_cuda: bool = EnvVar @ "USE_CUDA" | True,): """Types work with EnvVar.""" pass
Usage:
bash
LEARNING_RATE=0.01 BATCH_SIZE=64 python train.py# lr will be float(0.01), batch_size will be int(64)
Troubleshooting
Type Mismatch Errors
python
# Problem: None as default for non-optional type@proto.clidef bad(count: int = None): # Type error! pass# Solution: Use Optional@proto.clidef good(count: int | None = None): pass
Union Type Ambiguity
python
# Problem: Ambiguous union@proto.clidef ambiguous(value: int | str = 1): # CLI string "42" - is it int or str? pass# Solution: Use Literal or Enum for clarity@proto.clidef clear(value: Literal[1, 2, 3] = 1): pass