Parameter Overrides¶
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 |
|---|---|---|
|
Multiple instances |
Library code, reusable components |
|
Singleton (global) |
Namespaced config groups ( |
App decorator creates CLI entry point:
Decorator |
Use Case |
|---|---|
|
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.01CLI arguments -
--lr 0.01Environment variables -
LR=0.01Default values -
lr: float = 0.001
Override Methods¶
1. Command Line Arguments¶
The most common way to override parameters is through CLI arguments:
from params_proto import proto
@proto.cli
def 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:
# Override specific parameters
python train.py --lr 0.01 --batch-size 64
# Override all parameters
python train.py --lr 0.001 --batch-size 128 --epochs 200
# Boolean flags
python train.py --debug # Set debug=True
python train.py --no-debug # Set debug=False
2. Direct Assignment¶
Modify parameter values by directly assigning to the function or class:
@proto.cli
def train(
lr: float = 0.001,
batch_size: int = 32,
):
"""Train a model."""
print(f"lr={lr}, batch_size={batch_size}")
# Direct assignment before calling
train.lr = 0.01
train.batch_size = 64
train() # Uses lr=0.01, batch_size=64
With classes:
@proto
class Params:
lr: float = 0.001
batch_size: int = 32
# Modify class defaults
Params.lr = 0.01
Params.batch_size = 64
# New instances use updated defaults
config = Params()
print(config.lr) # 0.01
3. Function kwargs¶
Pass parameters directly when calling the function (highest priority):
@proto.cli
def 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 kwargs
train(lr=0.01, batch_size=64)
Kwargs override all other methods:
# Set via direct assignment
train.lr = 0.001
train.batch_size = 32
# Kwargs take precedence
train(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:
from params_proto import proto
@proto.prefix
class Model:
hidden_size: int = 256
num_layers: int = 4
@proto.prefix
class Training:
lr: float = 0.001
batch_size: int = 32
@proto.cli
def 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 binding
proto.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 exits
main() # Back to defaults or previous bindings
Binding precedence:
# Set default
train.lr = 0.001
# Bind new value (use direct keys for @proto.cli)
proto.bind(lr=0.01)
# Kwargs still take highest priority
train(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:
@proto.cli
def 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 assignment
train.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:
@proto.prefix
class Model:
name: str = "resnet50"
hidden_size: int = 256
@proto.prefix
class Training:
lr: float = 0.001
batch_size: int = 32
@proto.cli
def main():
"""Train model."""
print(f"Training {Model.name} with lr={Training.lr}")
# Method 1: Direct assignment
Model.name = "vit"
Training.lr = 0.01
main()
# Method 2: CLI with prefixes (kebab-case)
# python main.py --model.name vit --training.lr 0.01
# Method 3: proto.bind() with dotted names
proto.bind(**{
"Model.name": "vit",
"Training.lr": 0.01,
})
main()
Nested function parameters:
@proto.prefix
def train(
lr: float = 0.001,
batch_size: int = 32,
):
"""Training configuration."""
return {"lr": lr, "batch_size": batch_size}
@proto.cli
def main(seed: int = 42):
"""Main function."""
result = train()
print(f"seed={seed}, training={result}")
# Override nested function with dict
result = 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:
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:
@proto.cli
def 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 hyperparameters
best = 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():
@proto.prefix
class Model:
hidden_size: int = 256
num_layers: int = 4
dropout: float = 0.1
@proto.cli
def train():
"""Train model."""
print(f"Model config: {Model.hidden_size}, {Model.num_layers}, {Model.dropout}")
# Configuration profiles
PROFILES = {
"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 profile
def train_with_profile(profile: str):
"""Train with a specific profile."""
proto.bind(**PROFILES[profile])
train()
train_with_profile("large")
Dynamic Configuration¶
Build configurations dynamically:
@proto.cli
def 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:
@proto.cli
def 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 default
train(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:
@proto.cli
def train(lr: float = 0.001):
"""Train model."""
print(f"Training with lr={lr}")
# Default behavior
train() # 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 exits
train() # lr=0.001
Multiple contexts:
@proto.prefix
class Model:
name: str = "resnet50"
@proto.prefix
class Training:
lr: float = 0.001
@proto.cli
def main():
"""Train model."""
print(f"Training {Model.name} with lr={Training.lr}")
# Apply multiple overrides
with proto.bind(**{
"Model.name": "vit",
"Training.lr": 0.01,
}):
main() # Both overridden
Best Practices¶
1. Use CLI for User-Facing Scripts¶
# ✓ Good: CLI-driven configuration
@proto.cli
def train(lr: float = 0.001):
"""Train model."""
pass
if __name__ == "__main__":
train() # Users override via CLI
2. Use proto.bind() for Testing¶
# ✓ Good: Clean testing with context managers
def 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¶
# ✗ Avoid: Global side effects
train.lr = 0.01 # Affects all future calls
# ✓ Better: Explicit function calls
train(lr=0.01) # Clear and local
4. Document Override Methods¶
@proto.cli
def 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¶
@proto.cli
def 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
@proto.cli
def train(lr: float = 0.001):
"""Train model."""
print(f"lr={lr}")
train.lr = 0.01 # Set override
train() # Still shows lr=0.001 ???
Solution: Check if CLI parsing is overriding your value
# 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 parsing
if __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
@proto.prefix
class Model:
name: str = "resnet50"
# ✗ Wrong: No effect
proto.bind(Model_name="vit")
# ✓ Correct: Use dotted notation
proto.bind(**{"Model.name": "vit"})
Context Manager State¶
Problem: Values persist after context exit
# This should not happen, but check for:
# 1. Are you calling train() inside the context?
with proto.bind(**{"train.lr": 0.01}):
pass # Context exits
train() # Should use default lr=0.001
# 2. If values persist, there may be a bug
# Report at https://github.com/geyang/params-proto/issues