Skip to main content

Auto-generate Typer CLI interfaces from Pydantic models.

Project description

typantic

CI PyPI Python License

Auto-generate Typer CLI interfaces from Pydantic models.

Define your config once as a Pydantic model with validators, and get a fully-typed CLI for free — no duplication, no drift.

Installation

pip install typantic

Quick start

from pathlib import Path
from typing import Annotated

import typer
from pydantic import AfterValidator, BaseModel, Field

from typantic import pydantic_to_typer


# 1. Define your config with validators
class Config(BaseModel):
    images: Annotated[
        list[Path],
        Field(description="Image folders to process.", kw_only=False),
    ]
    output: Annotated[
        Path,
        AfterValidator(Path.resolve),
        Field(description="Output directory.", kw_only=True),
    ]
    threshold: Annotated[
        float,
        Field(default=0.5, description="Detection threshold.", kw_only=True),
    ]
    seed: Annotated[
        int | None,
        Field(default=None, description="Random seed.", kw_only=True),
    ]


# 2. Use the decorator — that's it
app = typer.Typer()

@app.command()
@pydantic_to_typer(Config)
def run(config: Config):
    """Process images with validation."""
    print(config)

if __name__ == "__main__":
    app()
$ python example.py --help

 Usage: example.py [OPTIONS] IMAGES...

 Process images with validation.

╭─ Arguments ──────────────────────────────────────────────────╮
│ *  images  IMAGES...  Image folders to process.  [required]  │
╰──────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────╮
│ *  --output     PATH     Output directory.  [required]       │
│    --threshold  FLOAT    Detection threshold.  [default: 0.5]│
│    --seed       INTEGER  Random seed.  [default: (None)]     │
│    --help                Show this message and exit.         │
╰──────────────────────────────────────────────────────────────╯

How it works

The @pydantic_to_typer(Model) decorator:

  1. Reads Model.model_fields to discover field names, types, descriptions, and defaults
  2. Strips Annotated validator metadata to extract the base types Typer understands
  3. Maps kw_only=Falsetyper.Argument, kw_only=Truetyper.Option
  4. Flattens nested BaseModel fields into prefixed parameters
  5. Rewrites the function's __signature__ so Typer sees the expanded parameters
  6. At call time, re-nests the raw CLI values and passes them into Model(...) so all Pydantic validators run

Your function receives the validated model instance — validators, default_factory, union types, and everything else works exactly as in Pydantic.

Features

Pydantic CLI result
kw_only=False typer.Argument (positional)
kw_only=True or unset typer.Option (--flag)
Field(description=...) help=... in the CLI
Field(default=...) Default value shown in --help
Field(default_factory=...) Re-evaluated on every invocation
Field(ge=..., le=...) Typer min / max (validated + shown)
Literal["a", "b"] CLI choices
Enum, tuple[...] Choices / multi-value option
nested BaseModel Flattened into --prefix-field options
SecretStr, SecretBytes Hidden input (secure prompt if required)
int | None Optional CLI option
default=None Rendered as [default: (None)]
list[Path] Variadic positional argument
AfterValidator, BeforeValidator Run at call time via Pydantic

Validators that raise ValueError / AssertionError surface as Typer parameter errors; other exception types propagate unchanged.

Per-field CLI hints

Customise individual flags with Field(json_schema_extra=...):

class Config(BaseModel):
    verbose: Annotated[
        bool,
        Field(default=False, json_schema_extra={"cli_short": "-v"}),
    ]
    output: Annotated[
        Path,
        Field(description="Output path.", json_schema_extra={"cli_name": "--dest"}),
    ]
    api_key: Annotated[
        str,
        Field(default="", json_schema_extra={"cli_envvar": "MYAPP_API_KEY"}),
    ]
Key Effect
cli_short Adds a short flag (e.g. -v) alongside the long one
cli_name Replaces the derived long flag (e.g. --dest)
cli_envvar Reads the value from an environment variable

Nested models

Fields whose type is itself a BaseModel are flattened into prefixed options, so layered configs map onto the CLI without manual wiring:

class Database(BaseModel):
    host: Annotated[str, Field(default="localhost", description="DB host.")]
    port: Annotated[int, Field(default=5432, ge=1, le=65535, description="DB port.")]


class Config(BaseModel):
    name: Annotated[str, Field(description="App name.", kw_only=False)]
    db: Database  # -> --db-host, --db-port
$ python example.py myapp --db-host db.internal --db-port 9000

The values are re-nested before the model is constructed, so Database's own validators and defaults apply as usual.

Registering commands without a stub

add_command wires a model and a handler onto a Typer app directly, skipping the decorate-a-stub-function boilerplate:

import typer

from typantic import add_command

app = typer.Typer()


def run(config: Config) -> None:
    print(config)


add_command(app, Config, run)            # command name defaults to "run"
add_command(app, Config, run, name="go")  # or set it explicitly

Help panels for mixin-composed models

Large configs composed from mixins can group their options into titled Rich help panels. Opt in with subpanels=True and give each mixin a cli_panel class attribute — every option lands in the panel of the class that defines its field:

from typing import Annotated, ClassVar

from pydantic import BaseModel, Field

from typantic import pydantic_to_typer


class ComputeMixin(BaseModel):
    cli_panel: ClassVar[str] = "Compute"

    cpus: Annotated[int, Field(default=4, description="CPU count.")]


class Config(ComputeMixin):
    dry_run: Annotated[bool, Field(default=False, description="Dry run.")]


@app.command()
@pydantic_to_typer(Config, subpanels=True)
def run(config: Config): ...
$ python example.py --help

 Usage: example.py [OPTIONS]

╭─ Options ──────────────────────────────────────────────────────╮
│ --dry-run    --no-dry-run    Dry run.  [default: no-dry-run]   │
│ --help                       Show this message and exit.       │
╰────────────────────────────────────────────────────────────────╯
╭─ Compute ──────────────────────────────────────────────────────╮
│ --cpus        INTEGER        CPU count.  [default: 4]          │
╰────────────────────────────────────────────────────────────────╯

--cpus renders under a "Compute" panel; --dry-run stays in the default options group (its defining class declares no cli_panel). Arguments are never panelled.

Requirements

  • Python ≥ 3.12
  • Pydantic ≥ 2.0
  • Typer ≥ 0.26

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typantic-0.3.0.tar.gz (9.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typantic-0.3.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file typantic-0.3.0.tar.gz.

File metadata

  • Download URL: typantic-0.3.0.tar.gz
  • Upload date:
  • Size: 9.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for typantic-0.3.0.tar.gz
Algorithm Hash digest
SHA256 1d95101240b1d85dc4584476ca4eee6d1b3d9bdc76897437b48605a225c98699
MD5 165f9e3a0e8ea42300f63238a742a67b
BLAKE2b-256 55da2fd25a33b0ab80e009d0c72e5252a8aea36828ad127c1ae4b7f29fdea7c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for typantic-0.3.0.tar.gz:

Publisher: publish.yml on KiSchnelle/typantic

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file typantic-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: typantic-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 10.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for typantic-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8b3828094fc17f76d047136f81f91b3cb37d7d1f33ba5834013495476fb87db
MD5 1da541b2179d50b6a2bcc96b16c289f4
BLAKE2b-256 0e018b53a051361de79245fb69c46dbf98df6e76210d9c6ec6cc9f69646012bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for typantic-0.3.0-py3-none-any.whl:

Publisher: publish.yml on KiSchnelle/typantic

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page