pydantic-settings CLI Migration Guide
This document describes CLI incompatibilities discovered when migrating looper from pydantic-argparse to pydantic-settings, along with decisions for each.
Background
Section titled “Background”looper previously used pydantic-argparse which only supports pydantic v1 (via pydantic.v1 shim). This shim breaks on Python 3.14+, and the library is unmaintained. We migrated to pydantic-settings which is maintained by the pydantic team and has native CLI support.
1. Multi-value list arguments require commas
Section titled “1. Multi-value list arguments require commas”With argparse/pydantic-argparse, list arguments supported space-separated values:
looper run --exc-flag completed running --dry-runpydantic-settings does NOT support this syntax. It treats running as an unrecognized positional argument.
Supported formats in pydantic-settings
Section titled “Supported formats in pydantic-settings”# Comma-separated (recommended)looper run --exc-flag completed,running --dry-run
# Repeated flagslooper run --exc-flag completed --exc-flag running --dry-run
# JSONlooper run --exc-flag '["completed","running"]' --dry-runDecision
Section titled “Decision”Breaking change accepted. Use comma-separated syntax for multi-value arguments. This is cleaner and more explicit.
Affected arguments
Section titled “Affected arguments”--exc-flag--sel-flag--sel-incl--amend--compute--sample-pipeline-interfaces--project-pipeline-interfaces
2. Short flags are case-insensitive
Section titled “2. Short flags are case-insensitive”argparse treats -p and -P as distinct options. pydantic-settings treats them as the same (case-insensitive), causing conflicts.
Current conflicting aliases:
-p→--package-P→--project-pipeline-interfaces
argparse.ArgumentError: argument -p/--project-pipeline-interfaces: conflicting option string: -pOptions
Section titled “Options”- Remove
-Palias from--project-pipeline-interfaces - Change
-Pto a different letter (e.g.,-Ifor Interfaces) - Use multi-character aliases (
--spi,--ppi) instead of single letters
Decision
Section titled “Decision”Changed to --spi and --ppi. These multi-character aliases are more descriptive and avoid case-sensitivity issues. No single-letter aliases for these arguments.
3. Short flags require AliasChoices
Section titled “3. Short flags require AliasChoices”pydantic-settings doesn't automatically create short aliases from field definitions. The cli_shortcuts config option exists but conflicts with cli_implicit_flags.
Solution
Section titled “Solution”Use pydantic.AliasChoices in Field definitions:
from pydantic import AliasChoices, Field
dry_run: bool = Field( default=False, validation_alias=AliasChoices('d', 'dry-run'), description="Don't actually submit jobs")Single-character aliases automatically become short flags (-d), multi-character become long flags (--dry-run).
Implementation
Section titled “Implementation”Updated Command.create_model() in commands.py to wire aliases into AliasChoices when creating dynamic models.
4. Boolean flags require cli_implicit_flags
Section titled “4. Boolean flags require cli_implicit_flags”By default, pydantic-settings requires explicit values for boolean arguments:
# Required by defaultlooper run --dry-run true
# What users expectlooper run --dry-runSolution
Section titled “Solution”Enable cli_implicit_flags=True in SettingsConfigDict:
model_config = SettingsConfigDict( cli_parse_args=True, cli_implicit_flags=True, # --dry-run works without value)This creates --flag / --no-flag pairs for boolean fields.
Decision
Section titled “Decision”Enabled. Preserves expected CLI behavior.
5. Help text customization
Section titled “5. Help text customization”pydantic-settings CLI help output exposes implementation details:
- Class docstrings appear as program description
- Type hints show
{bool,null}for optional fields - Subcommands have no descriptions by default
Solution
Section titled “Solution”Configure SettingsConfigDict and add Field descriptions:
model_config = SettingsConfigDict( cli_parse_args=True, cli_hide_none_type=True, # Hide {bool,null} type hints)
# Add descriptions for subcommandsrun: CliSubCommand[RunModel] = Field(description="Run or submit jobs.")Change class docstring from implementation notes to user-facing description.
Decision
Section titled “Decision”Configured. Clean help output matching argparse style.
Summary of Breaking Changes
Section titled “Summary of Breaking Changes”| Change | Old Syntax | New Syntax |
|---|---|---|
| Multi-value lists | --flag a b c | --flag a,b,c |
| Case-sensitive short flags | -p and -P distinct | Case-insensitive (conflict) |
Summary of Preserved Behavior
Section titled “Summary of Preserved Behavior”| Feature | Status |
|---|---|
Long flags (--dry-run) | ✓ Works |
Short flags (-d) | ✓ Works (via AliasChoices) |
| Boolean flags without value | ✓ Works (via cli_implicit_flags) |
Kebab-case (--dry-run not --dry_run) | ✓ Works (via cli_kebab_case) |