Concepts: CLI Generation¶
Dracon's command-line interface (CLI) generation bridges the gap between configuration management and application execution, providing a powerful and type-safe way to interact with your application.
The Core Idea: Model-Driven CLIs¶
Instead of manually writing argument parsing logic using libraries like argparse or click, Dracon generates the CLI directly from a Pydantic BaseModel.
Using @dracon_program (Recommended)¶
The simplest way to create a CLI program:
from dracon import dracon_program, Arg
from pydantic import BaseModel
from typing import Annotated
@dracon_program(name="my-app", description="My application")
class Config(BaseModel):
environment: Annotated[str, Arg(short='e', help="Deployment env")]
workers: int = 4
def run(self):
print(f"Running in {self.environment} with {self.workers} workers")
Config.cli() # parses sys.argv, loads config, calls .run()
The decorator adds .cli(), .invoke(), .from_config(), and .load() methods to your model class. See the CLI reference for all decorator options.
Using make_program (Low-Level)¶
For more control, use make_program directly:
- Define Configuration: Define your application's configuration parameters, their types, defaults, and help text using a Pydantic model.
- Annotate for CLI: Use
typing.Annotatedanddracon.Argto customize how specific fields map to CLI arguments (e.g., short flags, positional arguments). - Generate Program: Call
dracon.make_program(YourModel, ...)to create aPrograminstance. - Parse Arguments: Call
program.parse_args()which handles:- Parsing standard CLI flags (
--option value,--option=value,-o val) and positional arguments. - Generating a
--helpmessage automatically. - Handling special Dracon arguments:
+config/file.yaml: Loads and merges specified YAML configuration files.+config.yaml@sub.key: Loads a file and extracts a subtree.++VAR=valueor++VAR value: Sets context variables for interpolation (shorthand for--define.VAR=value).- Applying overrides from the CLI onto the defaults and loaded files.
- Validating the final configuration against your Pydantic model.
- Returning the validated Pydantic model instance.
- Parsing standard CLI flags (
Free Ordering¶
All argument types can be freely mixed in any order on the command line:
Dracon classifies each token by its prefix (+, ++, --, -, or none) and processes them accordingly, regardless of position.
Subcommands¶
Dracon supports subcommands (like git remote add or docker compose up) via Pydantic discriminated unions. The model defines the schema — no separate subcommand registration needed.
Each subcommand is a BaseModel with a discriminator field (action: Literal['name'] = 'name'), and the root model declares the subcommand field using Subcommand(*types):
from dracon import dracon_program, Arg, Subcommand
from pydantic import BaseModel
from typing import Annotated, Literal
class TrainCmd(BaseModel):
"""Train a model."""
action: Literal['train'] = 'train'
epochs: Annotated[int, Arg(help="Number of epochs")] = 10
def run(self, ctx):
print(f"Training for {self.epochs} epochs (verbose={ctx.verbose})")
class EvalCmd(BaseModel):
"""Evaluate a model."""
action: Literal['eval'] = 'eval'
dataset: Annotated[str, Arg(help="Test dataset path")]
def run(self, ctx):
print(f"Evaluating on {self.dataset}")
@dracon_program(name="ml-tool")
class CLI(BaseModel):
verbose: Annotated[bool, Arg(short='v')] = False
command: Subcommand(TrainCmd, EvalCmd)
Usage:
ml-tool train --epochs 50
ml-tool --verbose eval --dataset test.csv
ml-tool train --help # per-subcommand help
Shared Options¶
Options defined on the root model (like --verbose above) can appear before or after the subcommand name — both are equivalent:
Config File Scoping¶
Config files (+file) are scoped by position relative to the subcommand name:
- Before the subcommand → merges at root level
- After the subcommand → merges under the subcommand field only
ml-tool +base.yaml train # root-scoped
ml-tool train +training.yaml # subcommand-scoped (file just needs epochs/lr, no wrapper)
A full config file at root level can also specify the subcommand via the discriminator field:
Run Dispatch¶
When using @dracon_program, .cli() dispatches .run() automatically:
- If the root model has
.run()→ callsinstance.run()(developer controls everything) - Else if the subcommand has
.run(ctx)→ callssubcmd.run(root_instance)with the root config as context - Else → returns the config instance
Nested Subcommands¶
Subcommands can themselves contain Subcommand fields for multi-level nesting:
Subcommand models can also inherit from LazyDraconModel instead of BaseModel if their field defaults contain ${...} interpolations that should resolve using the program's context. See LazyDraconModel for details.
See the CLI customization guide and Subcommand reference for full details.
Configuration Loading and Precedence¶
This is where Dracon's CLI integrates seamlessly with its configuration loading capabilities. When program.parse_args() runs, configuration sources are applied in a specific order, with later sources overriding earlier ones:
- Pydantic Model Defaults: Initial values defined in your
BaseModel(field: int = 10). +file1.yaml: The first configuration file specified with+is loaded and composed (including its own includes, merges, etc.). Its values override the Pydantic defaults.+file2.yaml: The second+file is loaded and merged onto the result of step 2 (using Dracon's CLI merge strategy<<{<~}[<~]— replace mode, new wins for both dicts and lists).- ... Subsequent
+fileN.yamlfiles: Each is merged sequentially. ++VAR=valueContext: Variables defined via++(or the longer form--define.VAR=value) are added to the context, potentially influencing subsequent interpolations within CLI argument values or during final resolution.-
CLI Argument Overrides (
--key value, positional args): Values provided directly on the command line override any values from previous steps.- Nested Keys: Use dot notation (
--database.host db.prod) to target nested fields within your Pydantic model. Dracon internally builds the necessary nested dictionary structure. - File Values: If an argument value starts with
+(or the correspondingArghasis_file=True), Dracon loads the referenced file/key path and uses its content as the value for that specific CLI argument, merging it appropriately if the target field expects a mapping or sequence. Example:--database +db_override.yamlwould loaddb_override.yamland merge its content into thedatabasefield.
- Nested Keys: Use dot notation (
-
Pydantic Validation: The final, merged dictionary representing the configuration is validated against your Pydantic model (
AppConfig.model_validate(...)). This catches type errors, missing required fields, and runs custom validators.
Argument Mapping¶
- Field Names:
my_field_namebecomes--my-field-nameby default (underscores are replaced with dashes). Customizable withArg(long=...)or disabled per-field withArg(auto_dash_alias=False)to keep underscores. - Types: Pydantic types determine expected input (str, int, float, bool).
boolfields become flags (--verbose, no value needed). - Equals Syntax: All non-flag options support
--option=valuein addition to--option value. - Nested Models: Fields that are Pydantic models allow nested overrides using dot notation (
--database.port 5433or--database.port=5433). This works for any nested key, even if the developer didn't define an explicitArgfor it. Dracon handles constructing the nested dictionary. If the nested argument itself is marked withArg(is_file=True), passing a file path will load that file's content into that nested structure.
Collection Argument Support¶
Dracon automatically detects and handles collection types (lists, tuples, sets, dictionaries) with user-friendly command-line syntaxes:
- List-like arguments (
List[T],Tuple[T, ...],Set[T]) accept space-separated values:--tags web api backend - Dict-like arguments (
Dict[K, V]) accept key=value pairs:--config debug=true port=8080 - Nested dictionaries use dot notation:
--config app.name=myapp database.host=localhost - Traditional syntax is also supported:
--tags "['web', 'api']"or--config '{"debug": true}'
When a positional argument is a collection type, it consumes all remaining non-option arguments, so only one collection positional argument is allowed per command.
This integration means your CLI automatically respects your defined configuration structure, types, defaults, and validation rules, while also benefiting from Dracon's powerful file loading, merging, and interpolation features.
Debugging¶
DRACON_SHOW_VARS=1: Set this environment variable to print a table of all defined variables (CLI++vars, config!definevars) and their sources when running a CLI program.- Unused variable warnings: If a
++VARis defined but never referenced in any${VAR}expression, a warning is printed to help catch typos.