params-proto v3 provides multiple ways to override parameter values at runtime, from CLI arguments to programmatic assignment.
The Three Decorators
Config decorators define parameter schemas:
Decorator
Scope
Use Case
@proto
Multiple instances
Library code, reusable components
@proto.prefix
Singleton (global)
Namespaced config groups (Model.lr, Training.epochs)
App decorator creates CLI entry point:
Decorator
Use Case
@proto.cli
Wraps a function or class to parse CLI args
Typical pattern: Define configs with @proto/@proto.prefix, then create an entry point with @proto.cli.
Override Precedence
Parameters can be overridden in multiple ways, with clear precedence (highest to lowest):
Function kwargs - train(lr=0.1)
proto.bind() context - proto.bind(lr=0.01)
Direct assignment - train.lr = 0.01
CLI arguments - --lr 0.01
Environment variables - LR=0.01
Default values - lr: float = 0.001
Override Methods
1. Command Line Arguments
The most common way to override parameters is through CLI arguments:
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(f"Training with lr={lr}, batch_size={batch_size}")if __name__ == "__main__": train()
CLI usage:
bash
# Override specific parameterspython train.py --lr 0.01 --batch-size 64# Override all parameterspython train.py --lr 0.001 --batch-size 128 --epochs 200# Boolean flagspython train.py --debug # Set debug=Truepython train.py --no-debug # Set debug=False
2. Direct Assignment
Modify parameter values by directly assigning to the function or class:
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32,): """Train a model.""" print(f"lr={lr}, batch_size={batch_size}")# Direct assignment before callingtrain.lr = 0.01train.batch_size = 64train() # Uses lr=0.01, batch_size=64
With classes:
python
@protoclass Params: lr: float = 0.001 batch_size: int = 32# Modify class defaultsParams.lr = 0.01Params.batch_size = 64# New instances use updated defaultsconfig = Params()print(config.lr) # 0.01
3. Function kwargs
Pass parameters directly when calling the function (highest priority):
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32, epochs: int = 100,): """Train a model.""" print(f"lr={lr}, batch_size={batch_size}, epochs={epochs}")# Override via kwargstrain(lr=0.01, batch_size=64)
Kwargs override all other methods:
python
# Set via direct assignmenttrain.lr = 0.001train.batch_size = 32# Kwargs take precedencetrain(lr=0.01, batch_size=64) # Uses 0.01 and 64, not 0.001 and 32
4. proto.bind() Context
Use proto.bind() to set multiple overrides at once, especially useful for testing and parameter sweeps:
python
from params_proto import proto@proto.prefixclass Model: hidden_size: int = 256 num_layers: int = 4@proto.prefixclass Training: lr: float = 0.001 batch_size: int = 32@proto.clidef main(seed: int = 42): """Main entry point.""" print(f"Model: hidden_size={Model.hidden_size}, num_layers={Model.num_layers}") print(f"Training: lr={Training.lr}, batch_size={Training.batch_size}") print(f"seed={seed}")# Option 1: Function-style bindingproto.bind( seed=100, **{ "Model.hidden_size": 512, "Model.num_layers": 8, "Training.lr": 0.01, "Training.batch_size": 64, })main() # Uses all bound values# Option 2: Context manager (scoped)with proto.bind(seed=200, **{"Model.hidden_size": 1024}): main() # Uses seed=200, hidden_size=1024# Values reset after context exitsmain() # Back to defaults or previous bindings
Binding precedence:
python
# Set defaulttrain.lr = 0.001# Bind new value (use direct keys for @proto.cli)proto.bind(lr=0.01)# Kwargs still take highest prioritytrain(lr=0.1) # Uses 0.1, not 0.01
Override Precedence
When the same parameter is set multiple ways, params-proto uses this precedence order:
python
@proto.clidef train(lr: float = 0.001, batch_size: int = 32): """Train a model.""" print(f"lr={lr}, batch_size={batch_size}")# 1. Default value# lr=0.001, batch_size=32# 2. Environment variable (if using EnvVar)# LR=0.01 python train.py# 3. CLI arguments# python train.py --lr 0.01# 4. Direct assignmenttrain.lr = 0.01# 5. proto.bind() context (direct keys for @proto.cli)proto.bind(lr=0.01)# 6. Function kwargs (highest priority)train(lr=0.1) # This wins
Priority order (highest to lowest):
Function kwargs
proto.bind() context
Direct assignment
CLI arguments
Environment variables
Default values
Prefixed Configurations
Override prefixed configs using dotted notation:
python
@proto.prefixclass Model: name: str = "resnet50" hidden_size: int = 256@proto.prefixclass Training: lr: float = 0.001 batch_size: int = 32@proto.clidef main(): """Train model.""" print(f"Training {Model.name} with lr={Training.lr}")# Method 1: Direct assignmentModel.name = "vit"Training.lr = 0.01main()# Method 2: CLI with prefixes (kebab-case)# python main.py --model.name vit --training.lr 0.01# Method 3: proto.bind() with dotted namesproto.bind(**{ "Model.name": "vit", "Training.lr": 0.01,})main()
Nested function parameters:
python
@proto.prefixdef train( lr: float = 0.001, batch_size: int = 32,): """Training configuration.""" return {"lr": lr, "batch_size": batch_size}@proto.clidef main(seed: int = 42): """Main function.""" result = train() print(f"seed={seed}, training={result}")# Override nested function with dictresult = main(seed=100, train={"lr": 0.01, "batch_size": 64})# Override using proto.bind()proto.bind(**{ "seed": 200, "train.lr": 0.01, "train.batch_size": 64,})main()
Common Patterns
Testing
Override parameters in tests without modifying code:
python
def test_training(): """Test training with different configurations.""" from params_proto import proto @proto.cli def train(lr: float = 0.001, epochs: int = 100): """Train model.""" return {"lr": lr, "epochs": epochs} # Test with default values result = train() assert result["lr"] == 0.001 # Test with overrides result = train(lr=0.01, epochs=10) assert result["lr"] == 0.01 assert result["epochs"] == 10 # Test with proto.bind() (direct keys for @proto.cli) with proto.bind(lr=0.1): result = train() assert result["lr"] == 0.1
Parameter Sweeps
Use proto.bind() to run parameter sweeps:
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32, epochs: int = 100,): """Train model.""" print(f"Training: lr={lr}, batch_size={batch_size}") # ... training code ... return accuracy# Parameter sweep (direct keys for @proto.cli)results = []for lr in [0.001, 0.01, 0.1]: for batch_size in [32, 64, 128]: with proto.bind(lr=lr, batch_size=batch_size): accuracy = train() results.append({ "lr": lr, "batch_size": batch_size, "accuracy": accuracy, })# Find best hyperparametersbest = max(results, key=lambda x: x["accuracy"])print(f"Best: lr={best['lr']}, batch_size={best['batch_size']}")
Configuration Profiles
Create configuration profiles using proto.bind():
python
@proto.prefixclass Model: hidden_size: int = 256 num_layers: int = 4 dropout: float = 0.1@proto.clidef train(): """Train model.""" print(f"Model config: {Model.hidden_size}, {Model.num_layers}, {Model.dropout}")# Configuration profilesPROFILES = { "small": { "Model.hidden_size": 128, "Model.num_layers": 2, "Model.dropout": 0.1, }, "medium": { "Model.hidden_size": 256, "Model.num_layers": 4, "Model.dropout": 0.2, }, "large": { "Model.hidden_size": 512, "Model.num_layers": 8, "Model.dropout": 0.3, },}# Use a profiledef train_with_profile(profile: str): """Train with a specific profile.""" proto.bind(**PROFILES[profile]) train()train_with_profile("large")
Dynamic Configuration
Build configurations dynamically:
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32, epochs: int = 100,): """Train model.""" print(f"Training with lr={lr}, batch_size={batch_size}, epochs={epochs}")def load_config(config_file: str): """Load configuration from file.""" import json with open(config_file) as f: config = json.load(f) # Apply configuration using proto.bind() (direct keys for @proto.cli) proto.bind(**config)# Load from JSON file# config.json: {"lr": 0.01, "batch_size": 64, "epochs": 200}load_config("config.json")train() # Uses values from config.json
Partial Configuration
Override only some parameters:
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32, epochs: int = 100, warmup_steps: int = 1000,): """Train model.""" print(f"lr={lr}, batch_size={batch_size}, epochs={epochs}, warmup={warmup_steps}")# Override only lr and batch_size, keep others as defaulttrain(lr=0.01, batch_size=64)# lr=0.01, batch_size=64, epochs=100 (default), warmup_steps=1000 (default)# Using proto.bind() (direct keys for @proto.cli)with proto.bind(lr=0.01): train() # Only lr overridden
Context Manager Usage
The proto.bind() context manager is useful for scoped overrides:
python
@proto.clidef train(lr: float = 0.001): """Train model.""" print(f"Training with lr={lr}")# Default behaviortrain() # lr=0.001# Scoped override (direct keys for @proto.cli)with proto.bind(lr=0.01): train() # lr=0.01 # Nested contexts with proto.bind(lr=0.1): train() # lr=0.1 (inner context wins) train() # lr=0.01 (back to outer context)# Back to default after context exitstrain() # lr=0.001
# ✓ Good: Clean testing with context managersdef test_train(): with proto.bind(lr=0.01): # direct keys for @proto.cli result = train() assert result["lr"] == 0.01
3. Use Direct Assignment Sparingly
python
# ✗ Avoid: Global side effectstrain.lr = 0.01 # Affects all future calls# ✓ Better: Explicit function callstrain(lr=0.01) # Clear and local
4. Document Override Methods
python
@proto.clidef train(lr: float = 0.001): """Train a model. Args: lr: Learning rate CLI Usage: python train.py --lr 0.01 Programmatic: train(lr=0.01) # or proto.bind(lr=0.01) # direct keys for @proto.cli train() """ pass
5. Validate Overridden Values
python
@proto.clidef train( lr: float = 0.001, batch_size: int = 32,): """Train model.""" # Validate overridden values if lr <= 0 or lr >= 1: raise ValueError(f"lr must be in (0, 1), got {lr}") if batch_size < 1: raise ValueError(f"batch_size must be >= 1, got {batch_size}") print(f"Training with lr={lr}, batch_size={batch_size}")
Troubleshooting
Override Not Taking Effect
Problem: Parameter override doesn't work
python
@proto.clidef train(lr: float = 0.001): """Train model.""" print(f"lr={lr}")train.lr = 0.01 # Set overridetrain() # Still shows lr=0.001 ???
Solution: Check if CLI parsing is overriding your value
python
# When called as a script with CLI args, CLI wins# python train.py # Uses CLI defaults, not train.lr# To use programmatic overrides, call without CLI parsingif __name__ == "__main__": # Option 1: Set before CLI parsing train.lr = 0.01 train() # Parses CLI, your override may be lost # Option 2: Use kwargs (highest priority) train(lr=0.01) # Always works # Option 3: Use proto.bind() (direct keys for @proto.cli) proto.bind(lr=0.01) train()
Prefix Override Not Working
Problem: Dotted prefix not recognized
python
@proto.prefixclass Model: name: str = "resnet50"# ✗ Wrong: No effectproto.bind(Model_name="vit")# ✓ Correct: Use dotted notationproto.bind(**{"Model.name": "vit"})
Context Manager State
Problem: Values persist after context exit
python
# This should not happen, but check for:# 1. Are you calling train() inside the context?with proto.bind(**{"train.lr": 0.01}): pass # Context exitstrain() # Should use default lr=0.001# 2. If values persist, there may be a bug# Report at https://github.com/geyang/params-proto/issues