pydantic-autocli
Automatically generate sub-command based CLI applications from Pydantic models.
Installation
pip install pydantic-autocli
Features
- Automatic CLI generation from Pydantic models
- Type-safe argument parsing with validation
- Async command support
- Default command (
run_default) and subcommands - Nested command groups (
AutoCLI.mount) - Positional and remainder arguments (
--) viaExtraArgsMixin
Requires Python 3.10+ and Pydantic v2.
Basic Usage
from pydantic import BaseModel
from pydantic_autocli import AutoCLI, param
class MyCLI(AutoCLI):
# Default command: runs when no subcommand is provided
class DefaultArgs(BaseModel):
message: str = param("Hello", l="--message", s="-m")
def run_default(self, args: DefaultArgs):
print(args.message)
# Subcommand: `python script.py greet`
class GreetArgs(BaseModel):
name: str = param("World", l="--name", s="-n")
count: int = param(1, l="--count", s="-c")
def run_greet(self, args: GreetArgs):
for _ in range(args.count):
print(f"Hello, {args.name}!")
if __name__ == "__main__":
MyCLI().run()
# Default command
python script.py # prints "Hello"
python script.py --message "Hi" # prints "Hi"
# Subcommand
python script.py greet --name Alice # prints "Hello, Alice!"
python script.py greet -n Bob -c 3 # prints "Hello, Bob!" 3 times
# Help
python script.py --help # shows default command help
python script.py greet --help # shows greet command help
Note: --help is reserved and cannot be used as a field name.
Advanced Usage
Common Arguments and Initialization
from pydantic_autocli import AutoCLI, param
class MyCLI(AutoCLI):
# Shared arguments across all commands
class CommonArgs(AutoCLI.CommonArgs):
verbose: bool = param(False, l="--verbose", s="-v")
# Runs before every command
def prepare(self, args: CommonArgs):
if args.verbose:
print("Verbose mode enabled")
def run_task(self, args: CommonArgs):
print("Running task...")
Parameter Options
class TaskArgs(AutoCLI.CommonArgs):
# Required argument (no default)
name: str = param(..., l="--name", s="-n")
# Choices
mode: str = param("read", l="--mode", choices=["read", "write"])
# Validation
count: int = param(1, l="--count", ge=1, le=100)
pattern: str = param(".*", l="--pattern", pattern=r"^[a-z]+$")
Supported Types
| Annotation | CLI option |
|---|---|
str / int / float |
--opt VALUE |
Path, datetime, ... (parsed from a string or number) |
--opt VALUE |
bool |
flag --opt (defaults to False) |
Literal["a", "b"] / Enum |
--opt VALUE with choices (an Enum takes its values) |
list[X] |
--opt V1 V2 ... |
Optional[X] / X | None |
same as X; None when omitted |
- A
booloption is a flag that defaults toFalse. A default ofTrueis an error, because the flag could never be turned off. Use a separate negative field instead (e.g.no_cache: bool = False). - Other unions (e.g.
int | str) and unsupported types raise an error when the CLI is constructed.
Unknown Arguments
Arguments a command does not accept are errors (exit code 2), e.g. script.py greet --nmae foo → error: unrecognized arguments: --nmae foo.
Positional arguments and arguments after -- are accepted only when the args class uses ExtraArgsMixin (see below); unknown options are errors even then.
Async Commands and Return Values
async def run_fetch(self, args):
await asyncio.sleep(1)
if error:
return False # exit code 1
return True # exit code 0
# return 42 # custom exit code
Positional and Remainder Arguments (ExtraArgsMixin)
Use ExtraArgsMixin to capture positional arguments and arguments after --:
from pydantic import BaseModel
from pydantic_autocli import AutoCLI, param, ExtraArgsMixin
class MyCLI(AutoCLI):
class RunArgs(ExtraArgsMixin, BaseModel):
verbose: bool = param(False, l="--verbose", s="-v")
def run_exec(self, args: RunArgs):
# Positional arguments (before --)
files = args.get_positional() # ['file1.py', 'file2.py']
# Remainder arguments (after --)
cmd = args.get_remainder() # 'python -m pytest --tb=short'
cmd_list = args.get_remainder_list() # ['python', '-m', 'pytest', '--tb=short']
print(f"Files: {files}")
print(f"Command: {cmd}")
if __name__ == "__main__":
MyCLI().run()
# Positional args and remainder after --
python script.py exec file1.py file2.py -- python -m pytest --tb=short
# Only remainder
python script.py exec -- nested-command --option value
Note: For the default command (run_default), get_positional() always returns an empty list to avoid ambiguity with subcommand names. Use get_remainder() instead.
Command Groups (Nested Subcommands)
Assign another AutoCLI class to a run_* attribute to mount it as a command group.
class DbCLI(AutoCLI):
"""Database commands"""
class MigrateArgs(BaseModel):
dry: bool = param(False, l="--dry")
def run_migrate(self, args: MigrateArgs):
print(self.parent.verbose) # the mounting CLI is available as self.parent
class MyCLI(AutoCLI):
class CommonArgs(AutoCLI.CommonArgs):
verbose: bool = param(False, s="-v")
def prepare(self, args):
self.verbose = args.verbose
run_db = AutoCLI.mount(DbCLI, common=True) # script.py db migrate
run_cache = CacheCLI # same as AutoCLI.mount(CacheCLI)
python script.py db migrate --dry -v
python script.py db --help
common=False(default): the group is independent. The parent'sprepare()is not called.common=True: the parent'sCommonArgsoptions are accepted by every command in the group, and the parent'sprepare()runs before the group'sprepare(). A field name that conflicts with the parent'sCommonArgsraises an error (subclassing the parent'sCommonArgsis fine).- Groups can be nested. Each mounted CLI still works on its own with
DbCLI().run().
Argument Resolution
Args class is resolved in this order:
- Type annotation:
def run_cmd(self, args: MyArgs)→ usesMyArgs - Naming convention:
run_foo_bar→ looks forFooBarArgs - Fallback: uses
CommonArgs
class MyCLI(AutoCLI):
class FooBarArgs(BaseModel):
option: str = param("x", l="--option")
# Uses FooBarArgs by naming convention
def run_foo_bar(self, args):
print(args.option)
Development
uv sync # Install dependencies
uv run task test # Run tests
uv run task coverage # Run tests with coverage
uv run task lint # Lint code
uv run task example # Run example CLI
Claude Code Integration
Add to your project's CLAUDE.md:
## AutoCLI Usage
- `def run_foo_bar(self, a: FooBarArgs)` → `script.py foo-bar`
- `def run_default(self, a: DefaultArgs)` → `script.py` (no subcommand)
- `run_db = AutoCLI.mount(DbCLI, common=True)` → `script.py db <command>` (nested group)
- `class CommonArgs` → shared arguments across all commands
- `def prepare(self, a: CommonArgs)` → runs before every command
- Return `True`/`None` (exit 0), `False` (exit 1), `int` (custom exit code)
For details: `script.py --help` or `script.py <command> --help`
License
See LICENSE file.
Metadata
Release files for pydantic-autocli 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pydantic_autocli-0.4.0.tar.gz | 65.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_autocli-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 80.6 kB
Release files / pydantic_autocli-0.4.0.tar.gz
| Download URL | pydantic_autocli-0.4.0.tar.gz |
|---|---|
| Size | 65.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
21ad0518423b34526c377bcdf2cd2205a6c392cf5e688993e3ae2f9cc2149a79
|
|
BLAKE2b-256 checksum How to use checksums |
a17c9a1afc77c8cdef30402fe418be5c978c7dd6d2b86bef53d7fcafd0943782
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / pydantic_autocli-0.4.0-py3-none-any.whl
| Download URL | pydantic_autocli-0.4.0-py3-none-any.whl |
|---|---|
| Size | 15.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
16ba4c22859a8874c4501e1877c7713fd50a528bcb9af369746e9b6dd2139b1b
|
|
BLAKE2b-256 checksum How to use checksums |
419ae6c020f148cdcd3a5a63889d245d96cbc97e0e9a8782ad96ca0beb8ccafe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|