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:

  1. Inspects your function signature - Reads parameters, types, and defaults

  2. Extracts documentation - From inline comments and docstrings

  3. Converts names - Transforms Python naming to CLI conventions

  4. Generates argparse - Creates CLI parser automatically

  5. 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., HTTPServerhttp-server, MLModelml-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., DataLoaderdata-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

int

INT

--count INT

float

FLOAT

--lr FLOAT

str

STR

--name STR

bool

(flag)

--verbose

int | float

VALUE

--threshold VALUE

str | None

VALUE

--config VALUE

Literal["a", "b"]

VALUE

--mode VALUE

Enum

{A,B,C}

--opt {ADAM,SGD}

List[int]

VALUE

--ids VALUE

Path

VALUE

--dir VALUE

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

PerspectiveCamera

perspective-camera, perspectivecamera, PerspectiveCamera

HTTPServer

httpserver, http-server, HTTPServer

MLModel

mlmodel, ml-model, MLModel

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-case

  • Union classes: PascalCasekebab-case

  • Prefixes: PascalCase--kebab-case.kebab-case

  • Booleans: 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