Building Your CLI¶
params-proto automatically generates command-line interfaces from your Python code. There are two main ways to
do so: via a python class namespace, or via a function. Function interface is simple and useful for scripts,
but it lacks the ability to directly reference and expose configuration objects. For more detailed discussion, refer to
Best Practice for Function Parameters.
How It Works¶
When you use @proto.cli, params-proto:
Inspects your function signature - Reads parameters, types, and defaults
Extracts documentation - From inline comments and docstrings
Converts names - Transforms Python naming to CLI conventions
Generates argparse - Creates CLI parser automatically
Type converts - Parses and validates CLI arguments
from params_proto import proto
@proto.cli
def train(
learning_rate: float = 0.001, # Learning rate
batch_size: int = 32, # Batch size
):
"""Train a model."""
pass
↓ Automatically becomes ↓
$ python train.py --help
usage: train.py [-h] [--learning-rate FLOAT] [--batch-size INT] Train a model. options: -h, --help show this help message and exit --learning-rate FLOAT Learning rate (default: 0.001) --batch-size INT Batch size (default: 32)
Naming Conventions¶
Parameter Names: snake_case → kebab-case¶
Python parameters convert to CLI arguments:
@proto.cli
def train(
learning_rate: float = 0.001, # Python: snake_case
batch_size: int = 32,
max_epochs: int = 100,
):
pass
CLI arguments:
--learning-rate # Converted to kebab-case
--batch-size
--max-epochs
Conversion rule: Replace _ with -, then lowercase
Class Names: PascalCase → kebab-case¶
When using Union types for subcommands, class names convert to CLI commands:
from dataclasses import dataclass
@dataclass
class Train: # PascalCase in Python
lr: float = 0.001
@dataclass
class Evaluate:
model: str
@proto.cli
def tool(command: Train | Evaluate):
"""Tool with subcommands."""
pass
CLI commands:
python tool.py train --lr 0.01 # Class: Train → command: train
python tool.py evaluate --model pt # Class: Evaluate → command: evaluate
Conversion rule: PascalCase → kebab-case (e.g., HTTPServer → http-server, MLModel → ml-model)
Prefix Names: PascalCase → kebab-case¶
When using @proto.prefix, class names convert to kebab-case prefixes:
@proto.prefix
class Model: # PascalCase in Python
name: str = "resnet50"
hidden_size: int = 256
@proto.prefix
class Training: # PascalCase in Python
lr: float = 0.001
CLI arguments:
--model.name resnet50 # Prefix converts to kebab-case
--model.hidden-size 512 # Parameter converts to kebab-case
--training.lr 0.01 # Prefix converts to kebab-case
Conversion rule: Class name converts to kebab-case (e.g., DataLoader → data-loader), parameters convert to kebab-case.
# In code
print(Model.name) # PascalCase class
# On CLI
--model.name resnet50 # kebab-case prefix
Naming Best Practices¶
1. Use Simple Names for Union Types¶
# ✓ Good: Simple single-word names
class Train: # → train
class Evaluate: # → evaluate
class Export: # → export
# ✓ Good: Acronyms now convert properly
class HTTPServer: # → http-server
class MLModel: # → ml-model
class DataLoader: # → data-loader
# ✓ Also good: Simple single-word names
class Server: # → server (simple and clear)
class Model: # → model
2. Use snake_case for Parameters¶
# ✓ Good: snake_case converts perfectly
@proto.cli
def train(
learning_rate: float = 0.001, # → --learning-rate
batch_size: int = 32, # → --batch-size
):
pass
# ✗ Avoid: camelCase doesn't split
@proto.cli
def train(
learningRate: float = 0.001, # → --learningrate (no hyphen!)
):
pass
3. Keep Prefix Names Simple¶
# ✓ Good: Simple and clear
@proto.prefix
class Model: # --Model.param
class Training: # --Training.param
# ⚠️ Works but verbose
@proto.prefix
class DataLoader: # --DataLoader.param (long)
# ✓ Better
@proto.prefix
class Data: # --Data.param (shorter)
Help Text Generation¶
Inline Comments¶
@proto.cli
def train(
lr: float = 0.001, # This becomes the CLI help text
batch_size: int = 32, # Keep it short and descriptive
):
pass
Generated help:
--lr FLOAT This becomes the CLI help text (default: 0.001) --batch-size INT Keep it short and descriptive (default: 32)
Docstring Args Section¶
@proto.cli
def train(
lr: float = 0.001, # Learning rate
):
"""Train a model.
Args:
lr: Learning rate for the optimizer. Typical values are 0.001 for
Adam and 0.01-0.1 for SGD. Reduce if training is unstable.
"""
pass
Generated help combines both:
--lr FLOAT Learning rate Learning rate for the optimizer. Typical values are 0.001 for Adam and 0.01-0.1 for SGD. Reduce if training is unstable. (default: 0.001)
Function Docstring¶
The function’s main docstring becomes the CLI description:
@proto.cli
def train(lr: float = 0.001):
"""Train a neural network on CIFAR-10.
This function implements the full training loop including
data loading, forward/backward passes, and checkpointing.
"""
pass
Generated help:
usage: train.py [-h] [--lr FLOAT]
Train a neural network on CIFAR-10.
options:
...
All text before the first section header (Args:, Returns:, Raises:, etc.) appears in the help text. This allows multi-paragraph descriptions as long as they come before any structured documentation sections.
Auto-Generated Descriptions¶
If no documentation provided, params-proto generates basic descriptions from parameter names:
@proto.cli
def train(
learning_rate: float = 0.001, # No comment
batch_size: int = 32, # No comment
):
"""Train a model."""
pass
Generated help:
--learning-rate FLOAT Learning rate (default: 0.001) --batch-size INT Batch size (default: 32)
Type Display¶
Parameter types appear in help text:
Python Type |
CLI Display |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
(flag) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Boolean Flags¶
Boolean parameters become flags:
@proto.cli
def train(
verbose: bool = False, # Flag (no argument)
cuda: bool = True, # Flag (no argument)
):
pass
CLI usage:
# Set to True
python train.py --verbose
# Set to False
python train.py --no-verbose
# Boolean with True default
python train.py --cuda # Still True (default)
python train.py --no-cuda # Now False
Required vs Optional¶
Optional parameters (with defaults):
@proto.cli
def train(
lr: float = 0.001, # Optional
epochs: int = 100, # Optional
):
pass
Help text:
--lr FLOAT Learning rate (default: 0.001) --epochs INT Number of epochs (default: 100)
Required parameters (no defaults):
@proto.cli
def train(
data_path: str, # Required!
lr: float = 0.001,
):
pass
Help text:
--data-path STR Data path (required) --lr FLOAT Learning rate (default: 0.001)
Grouped Options¶
@proto.prefix groups options in help text:
@proto.prefix
class Model:
"""Model architecture."""
name: str = "resnet50"
hidden_size: int = 256
@proto.prefix
class Training:
"""Training hyperparameters."""
lr: float = 0.001
batch_size: int = 32
@proto.cli
def main(seed: int = 42):
"""Train model."""
pass
Generated help:
usage: main.py [-h] [--seed INT] [OPTIONS] Train model. options: -h, --help show this help message and exit --seed INT Random seed (default: 42) Model options: Model architecture. --Model.name STR Model name (default: resnet50) --Model.hidden-size INT Hidden size (default: 256) Training options: Training hyperparameters. --Training.lr FLOAT Learning rate (default: 0.001) --Training.batch-size INT Batch size (default: 32)
Union and Class Subcommands¶
Union types and dataclasses can be used to create subcommand-like CLIs, where each class represents a different configuration option.
Basic Union Subcommands¶
from dataclasses import dataclass
@dataclass
class PerspectiveCamera:
"""Perspective camera with field of view."""
fov: float = 60.0
aspect: float = 1.33
@dataclass
class OrthographicCamera:
"""Orthographic camera with uniform scale."""
scale: float = 1.0
@proto.cli
def render(
camera: PerspectiveCamera | OrthographicCamera,
output: str = "render.png",
):
"""Render a scene with a camera."""
pass
CLI usage - Multiple syntaxes supported:
# PascalCase (exact match)
python render.py --camera:PerspectiveCamera --output scene.png
# kebab-case (normalized)
python render.py --camera:perspective-camera --output scene.png
# lowercase (normalized)
python render.py --camera:perspectivecamera --output scene.png
# Positional (for required Union parameters)
python render.py perspective-camera --output scene.png
Single Class Parameters¶
The same syntax works for single class types (not just Unions):
@dataclass
class CameraConfig:
fov: float = 60.0
near: float = 0.1
far: float = 100.0
@proto.cli
def render(camera: CameraConfig):
"""Render with a camera."""
pass
CLI usage:
# Any of these work
python render.py --camera:CameraConfig
python render.py --camera:camera-config
python render.py camera-config
Setting Class Attributes¶
You can override class attributes from the command line:
@dataclass
class PerspectiveCamera:
fov: float = 60.0
aspect: float = 1.33
near: float = 0.1
@proto.cli
def render(camera: PerspectiveCamera):
"""Render with a camera."""
pass
CLI usage:
# Select class and override attributes
python render.py --camera:PerspectiveCamera --camera.fov 45 --camera.aspect 1.77
# Works with normalized names too
python render.py --camera:perspective-camera --camera.fov 45
# Positional class selection with attributes
python render.py perspective-camera --camera.fov 45
Syntax Variations¶
For Union type selection (like --camera:ClassName), all naming conventions work:
Python Class |
CLI Syntax Options |
|---|---|
|
|
|
|
|
|
Note
For prefix parameter access (like --http-server.port), you must use the exact registered prefix name, which is the kebab-case version of the class name (e.g., http-server for HTTPServer).
Attribute names always convert to kebab-case:
@dataclass
class Config:
batch_size: int = 32 # → --config.batch-size
learning_rate: float = 0.001 # → --config.learning-rate
Union with Regular Parameters¶
Mix Union/class parameters with regular parameters:
@dataclass
class PerspectiveCamera:
fov: float = 60.0
@dataclass
class OrthographicCamera:
scale: float = 1.0
@proto.cli
def render(
camera: PerspectiveCamera | OrthographicCamera, # Union parameter
output: str = "render.png", # Regular parameter
verbose: bool = False, # Boolean flag
):
"""Render a scene."""
pass
CLI usage:
python render.py --camera:PerspectiveCamera --output scene.png --verbose
python render.py perspective-camera --verbose --output scene.png
Best Practices¶
✓ Good: Simple class names
@dataclass
class Perspective: # → perspective
fov: float = 60.0
@dataclass
class Orthographic: # → orthographic
scale: float = 1.0
✓ Good: Descriptive attributes
@dataclass
class Camera:
field_of_view: float = 60.0 # → --camera.field-of-view
aspect_ratio: float = 1.33 # → --camera.aspect-ratio
⚠️ Works but less clear
@dataclass
class PerspectiveCameraConfig: # → perspective-camera-config (long!)
fov: float = 60.0
Custom Program Name¶
Override the program name in help text:
@proto.cli(prog="train_model")
def train(lr: float = 0.001):
"""Train a model."""
pass
Generated help:
usage: train_model [-h] [--lr FLOAT]
Without prog, uses script filename from sys.argv[0].
Testing Help Generation¶
Access help text programmatically:
@proto.cli
def train(lr: float = 0.001):
"""Train a model."""
pass
# Access generated help string
print(train.__help_str__)
Useful for testing and documentation generation.
Edge Cases¶
Long Parameter Names¶
@proto.cli
def train(
this_is_a_very_long_parameter_name: int = 1000,
):
"""Train with long names."""
pass
Generated help (wraps nicely):
--this-is-a-very-long-parameter-name INT This is a very long parameter name (default: 1000)
Numbers in Names¶
@proto.cli
def train(
model_2d: bool = False, # → --model-2d
resnet50_pretrained: bool = True, # → --resnet50-pretrained
):
pass
Numbers are preserved in CLI arguments.
Summary¶
Key conversions:
Parameters:
snake_case→--kebab-caseUnion classes:
PascalCase→kebab-casePrefixes:
PascalCase→--kebab-case.kebab-caseBooleans:
bool→--flag/--no-flag
Best practices:
Use simple class names for Union types
Use snake_case for all parameters
Document with inline comments + docstrings
Keep prefix names short and clear