Configuration Basics¶
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 |
|
|
Best for |
Script entry points, CLI tools |
Reusable configs, multiple instances |
CLI |
Automatic |
Manual (via |
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:
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(lr, batch_size, epochs)
if __name__ == "__main__":
train()
CLI usage:
python train.py --lr 0.01 --batch-size 64 --epochs 200
When to Use Functions¶
✅ Perfect for:
Script entry points (
if __name__ == "__main__")CLI tools and utilities
Simple configuration with few parameters
When you need immediate CLI integration
❌ Not ideal for:
Reusable configuration objects
When you need multiple instances
Library code (use
@protoclasses instead)
Documentation Extraction¶
Function documentation comes from two sources:
1. Inline comments:
@proto.cli
def train(
lr: float = 0.001, # This becomes the help text
batch_size: int = 32, # Short and sweet
):
pass
2. Docstring Args section:
@proto.cli
def 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
@dataclass
class Train:
lr: float = 0.001
epochs: int = 100
@dataclass
class Evaluate:
model: str # Required!
batch_size: int = 64
@proto.cli
def tool(
command: Train | Evaluate, # Required - no default
verbose: bool = False,
):
"""Multi-command tool."""
if isinstance(command, Train):
print(f"Training: lr={command.lr}")
elif isinstance(command, Evaluate):
print(f"Evaluating: {command.model}")
if __name__ == "__main__":
tool()
CLI usage:
python tool.py train --lr 0.01
python tool.py evaluate --model checkpoint.pt
See Types Guide for details.
Class-Based Configurations¶
Basic Configuration Class¶
Create reusable configuration objects:
from params_proto import proto
@proto
class TrainConfig:
"""Training configuration."""
lr: float = 0.001 # Learning rate
batch_size: int = 32 # Batch size
epochs: int = 100 # Number of epochs
Creating and Using Instances¶
# Create instances with defaults
config1 = TrainConfig()
print(config1.lr) # 0.001
# Create with custom values
config2 = TrainConfig(lr=0.01, epochs=200)
print(config2.lr) # 0.01
# Instances are independent
config1.lr = 0.001
config2.lr = 0.01
print(config1.lr, config2.lr) # 0.001 0.01
Class-Level Access¶
# Access class defaults
print(TrainConfig.lr) # 0.001
# Modify class default (affects new instances)
TrainConfig.lr = 0.01
config3 = TrainConfig()
print(config3.lr) # 0.01 (uses new default)
When to Use Classes¶
✅ Perfect for:
Reusable configuration objects
Library code and frameworks
When you need multiple independent instances
Configuration hierarchies (inheritance)
Integration with dataclasses
❌ Not ideal for:
Simple scripts (use
@proto.clifunctions)Direct CLI entry points (wrap with
@proto.cli)
Inheritance¶
Build configuration hierarchies:
@proto
class BaseConfig:
"""Base configuration."""
lr: float = 0.001
batch_size: int = 32
@proto
class AdamConfig(BaseConfig):
"""Adam optimizer configuration."""
beta1: float = 0.9
beta2: float = 0.999
# Inherits lr and batch_size
config = AdamConfig()
print(config.lr, config.beta1) # 0.001 0.9
Dataclass Integration¶
Combine with dataclasses for extra features:
from dataclasses import dataclass
@proto
@dataclass
class 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 work
config = 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:
@proto
class TrainConfig:
lr: float = 0.001
batch_size: int = 32
@proto.cli
def 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:
@proto
class Config:
lr: float = 0.01
batch_size: int = 32
@classmethod
def from_preset(cls, preset: str = "default"):
"""Create config from a preset."""
if preset == "large":
cls.lr = 0.001
cls.batch_size = 128
return cls()
@staticmethod
def validate_lr(lr: float) -> bool:
"""Validate learning rate."""
return 0 < lr < 1.0
def summary(self):
"""Return config summary."""
return f"lr={self.lr}, batch_size={self.batch_size}"
Usage:
# Classmethod receives the correct cls
config = Config.from_preset("large")
print(config.lr) # 0.001
# Staticmethod works as expected
assert Config.validate_lr(0.01) is True
# Instance methods work normally
config = Config()
print(config.summary()) # lr=0.01, batch_size=32
Key points:
Methods are not included in configuration parameters (only type-annotated class attributes are)
@classmethodreceives the correct wrapped class, so modifications toclsattributes reflect in the config@staticmethodworks identically to regular classesInstance methods have access to instance attributes via
self
Post-Initialization Hook¶
@proto classes support __post_init__ (like dataclasses) for validation and computed attributes:
@proto
class TrainConfig:
lr: float = 0.01
batch_size: int = 32
total_samples: int = None # Computed
def __post_init__(self):
# Validation
if self.lr > 1:
raise ValueError("lr must be <= 1")
# Computed attributes
self.total_samples = self.batch_size * 100
Usage:
config = TrainConfig(lr=0.5, batch_size=64)
print(config.total_samples) # 6400
TrainConfig(lr=2.0) # Raises ValueError
Key points:
__post_init__runs after all attributes are setAccess attributes via
self.attrUse for validation, computed values, or side effects
Works with both
@protoand@proto.prefixclasses
Choosing Between Functions and Classes¶
Use Functions When:¶
# ✓ Script entry point
@proto.cli
def main(lr: float = 0.001):
"""Train model."""
pass
if __name__ == "__main__":
main()
Use Classes When:¶
# ✓ Reusable configuration
@proto
class ExperimentConfig:
seed: int = 42
name: str = "exp"
# Create multiple experiments
exp1 = ExperimentConfig(seed=1, name="baseline")
exp2 = ExperimentConfig(seed=2, name="ablation")
Use Both Together:¶
# Classes for config structure
@proto
class ModelConfig:
hidden_size: int = 256
num_layers: int = 4
@proto
class DataConfig:
batch_size: int = 32
num_workers: int = 4
# Function for CLI entry point
@proto.cli
def train(
model: ModelConfig = ModelConfig(),
data: DataConfig = DataConfig(),
epochs: int = 100,
):
"""Train with structured config."""
print(f"Model: {model.hidden_size} hidden, {model.num_layers} layers")
print(f"Data: batch_size={data.batch_size}")
if __name__ == "__main__":
train()
Type Annotations¶
All parameters must have type annotations:
@proto.cli
def train(
lr: float = 0.001, # ✓ Has type annotation
epochs: int = 100, # ✓ Has type annotation
# count = 10, # ✗ Missing type annotation (won't work)
):
pass
Supported types:
Basic:
int,float,str,boolOptional:
str | None,Optional[int]Collections:
List[int],Tuple[int, int]Literal:
Literal["adam", "sgd"]Enum:
Optimizer.ADAMPath:
pathlib.PathUnion:
Train | Evaluate
See Type System for complete reference.
Documentation Best Practices¶
1. Use Inline Comments¶
# ✓ Good: inline comments for quick reference
@proto.cli
def train(
lr: float = 0.001, # Learning rate
batch_size: int = 32, # Training batch size
epochs: int = 100, # Number of epochs
):
pass
2. Add Docstring for Details¶
# ✓ Good: docstring for comprehensive documentation
@proto.cli
def train(
lr: float = 0.001, # Learning rate
):
"""Train a neural network.
Args:
lr: Learning rate for optimizer. Typical values:
- 0.001 for Adam (default)
- 0.01-0.1 for SGD
Start high and reduce if training is unstable.
"""
pass
3. Use Type Hints for Constraints¶
# ✓ Good: Literal types document valid values
from typing import Literal
@proto.cli
def train(
optimizer: Literal["adam", "sgd", "rmsprop"] = "adam", # Optimizer type
):
"""Train with specific optimizers only."""
pass
Common Patterns¶
Factory Pattern¶
@proto
class Params:
model_type: str = "resnet"
hidden_size: int = 256
def create_model(config: Params):
"""Create model from config."""
if config.model_type == "resnet":
return ResNet(config.hidden_size)
elif config.model_type == "transformer":
return Transformer(config.hidden_size)
config = Params(model_type="transformer")
model = create_model(config)
Configuration Registry¶
@proto
class ResNetConfig:
num_layers: int = 50
@proto
class TransformerConfig:
num_heads: int = 8
CONFIG_REGISTRY = {
"resnet": ResNetConfig,
"transformer": TransformerConfig,
}
def get_config(name: str):
"""Get config class by name."""
return CONFIG_REGISTRY[name]()
config = get_config("resnet")