Skip to content

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.

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:

Terminal window
looper run --exc-flag completed running --dry-run

pydantic-settings does NOT support this syntax. It treats running as an unrecognized positional argument.

Terminal window
# Comma-separated (recommended)
looper run --exc-flag completed,running --dry-run
# Repeated flags
looper run --exc-flag completed --exc-flag running --dry-run
# JSON
looper run --exc-flag '["completed","running"]' --dry-run

Breaking change accepted. Use comma-separated syntax for multi-value arguments. This is cleaner and more explicit.

  • --exc-flag
  • --sel-flag
  • --sel-incl
  • --amend
  • --compute
  • --sample-pipeline-interfaces
  • --project-pipeline-interfaces

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: -p
  1. Remove -P alias from --project-pipeline-interfaces
  2. Change -P to a different letter (e.g., -I for Interfaces)
  3. Use multi-character aliases (--spi, --ppi) instead of single letters

Changed to --spi and --ppi. These multi-character aliases are more descriptive and avoid case-sensitivity issues. No single-letter aliases for these arguments.


pydantic-settings doesn't automatically create short aliases from field definitions. The cli_shortcuts config option exists but conflicts with cli_implicit_flags.

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).

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:

Terminal window
# Required by default
looper run --dry-run true
# What users expect
looper run --dry-run

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.

Enabled. Preserves expected CLI behavior.


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

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 subcommands
run: CliSubCommand[RunModel] = Field(description="Run or submit jobs.")

Change class docstring from implementation notes to user-facing description.

Configured. Clean help output matching argparse style.


ChangeOld SyntaxNew Syntax
Multi-value lists--flag a b c--flag a,b,c
Case-sensitive short flags-p and -P distinctCase-insensitive (conflict)
FeatureStatus
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)