params-proto v3 offers two ways to define configurations: function-based and class-based. Both use type hints and decorators, but serve different purposes.
Quick Comparison
Aspect
Function-Based
Class-Based
Decorator
@proto.cli
@proto or @proto.prefix
Best for
Script entry points, CLI tools
Reusable configs, multiple instances
CLI
Automatic
Manual (via @proto.cli wrapper)
Instances
One per call
Create as many as needed
Access
Parameters
Attributes
Rule of thumb: Use functions for scripts, classes for libraries and reusable components.
Function-Based Configurations
Basic CLI Function
The simplest way to create a CLI:
python
from params_proto import proto@proto.clidef train( lr: float = 0.001, # Learning rate batch_size: int = 32, # Batch size epochs: int = 100, # Number of epochs): """Train a model.""" print(lr, batch_size, epochs)if __name__ == "__main__": train()
@proto.clidef train( lr: float = 0.001, # This becomes the help text batch_size: int = 32, # Short and sweet): pass
2. Docstring Args section:
python
@proto.clidef train( lr: float = 0.001, # Learning rate batch_size: int = 32, # Batch size): """Train a model. Args: lr: Learning rate for the optimizer. Start with 0.001 and adjust based on convergence. Higher values train faster but may be unstable. batch_size: Training batch size. Larger values use more memory but provide more stable gradients. """ pass
The help text combines both: inline comment first, then docstring details.
Required Parameters
Use Union types with required parameters for subcommand-like behavior:
from dataclasses import dataclass@proto@dataclassclass Params: """Model configuration.""" hidden_size: int = 256 num_layers: int = 4 dropout: float = 0.1 def __post_init__(self): """Validate after initialization.""" if self.dropout < 0 or self.dropout > 1: raise ValueError("dropout must be in [0, 1]")# Dataclass features workconfig = Params(hidden_size=512)print(config) # Params(hidden_size=512, num_layers=4, dropout=0.1)
Using Classes with CLI
Wrap class instances in a @proto.cli function:
python
@protoclass TrainConfig: lr: float = 0.001 batch_size: int = 32@proto.clidef train(config: TrainConfig = TrainConfig()): """Train with configuration.""" print(f"Training with lr={config.lr}")if __name__ == "__main__": train()
Or use @proto.prefix for global configuration (see Advanced Patterns).
Methods in Configuration Classes
@proto classes can include methods just like regular classes. Methods (classmethod, staticmethod, and instance methods) work as expected:
Function-based parameter definitions have a limitation: they break the linkage between parameter definitions and their usage. Functions create a new scope for local variables, disconnecting parameters from their original definitions.
Consider:
python
@proto.clidef train(lr: float = 0.001, batch_size: int = 32): """Train a model.""" print(f"Learning Rate: {lr}") # ❌ No way to link 'lr' back to a centralized parameter definition
In vanilla Python, you cannot easily access or iterate over function parameter defaults like you can with class attributes.
How params-proto Solves This: ProtoWrapper
When you decorate a function with @proto, params-proto doesn't just inspect the function—it wraps it in a special ProtoWrapper object. This wrapper provides the attribute access interface that vanilla Python functions lack.
The ProtoWrapper intercepts attribute access and function calls to enable the same ergonomic API that classes provide:
python
@protodef train(lr: float = 0.01, batch_size: int = 32): print(f"Training with lr={lr}, batch_size={batch_size}")# ProtoWrapper allows this:train.lr = 0.001 # Store overrideprint(train.lr) # Read current value → 0.001# And enables sweeps like this:for train.lr in [0.001, 0.01, 0.1]: train() # Each call uses the updated lr value
Behind the scenes:
Parameter defaults are extracted from the function signature and stored internally
Overrides are tracked in a separate dictionary
Attribute access checks overrides first, then falls back to defaults
Function calls merge defaults, overrides, and any kwargs before passing them to the original function
Alternative Approaches
If you don't want to use @proto for functions, here are traditional workarounds:
Option 1: Argument Data Class
python
@dataclassclass TrainParams: lr: float = 0.01 batch_size: int = 32def train(params: TrainParams) -> None: # ✓ Your IDE will link 'params.lr' back to the TrainParams definition print(f"Learning Rate: {params.lr}")config = TrainParams(lr=0.001)train(params=config)