Typer
Typer generates CLIs from Python type hints. Built on Click, it reduces boilerplate while keeping Click's ecosystem.
Recipe
import typer
app = typer.Typer()
@app.command()
def greet(name: str, loud: bool = False):
msg = f"Hello, {name}"
typer.echo(msg.upper() if loud else msg)
if __name__ == "__main__":
app()When to reach for this:
- Project already uses type hints throughout
- Fast CLI prototyping from function signatures
- Click power with less decorator boilerplate
Working Example
import typer
from pathlib import Path
from typing import Optional
from enum import Enum
app = typer.Typer(help="Invoice management CLI")
class OutputFormat(str, Enum):
json = "json"
csv = "csv"
@app.command()
def export(
invoice_id: int = typer.Argument(..., help="Invoice ID"),
output: Path = typer.Option("out.json", help="Output file"),
format: OutputFormat = OutputFormat.json,
verbose: bool = typer.Option(False, "--verbose", "-v"),
):
if verbose:
typer.echo(f"Exporting invoice {invoice_id} as {format.value}", err=True)
# export logic
typer.echo(f"Written to {output}")
@app.command()
def list(limit: int = typer.Option(10, min=1, max=100)):
typer.echo(f"Listing {limit} invoices")
if __name__ == "__main__":
app()What this demonstrates:
- Type hints drive argument types and help text
EnumbecomesChoiceautomaticallyPathvalidates file pathstyper.Optionandtyper.Argumentfor metadata
Deep Dive
Type Mapping
| Type hint | CLI behavior |
|---|---|
str | String argument |
int | Integer with validation |
bool | Flag (default False) |
Optional[str] | Optional option |
Enum | Choice from enum values |
Path | File/directory path |
Gotchas
- Missing type hints - Typer cannot infer arguments. Fix: annotate every parameter.
- Complex validation - hints are not enough. Fix: use Pydantic models or callback validators.
- Bool positional confusion -
boolas positional is tricky. Fix: usetyper.Optionfor flags. - Not installing typer[all] - missing shell completion extras. Fix:
uv add "typer[all]"for dev. - Breaking changes between Typer versions - pin version. Fix: lock in pyproject.toml.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Click | More control over parsing | Type hints cover your needs |
| argparse | Zero dependencies | Type-driven CLI wanted |
| cyclopts | Dataclass-heavy config | Simple function commands |
FAQs
Typer or Click?
Typer for type-hint projects. Click for fine-grained control.
How do I test Typer?
from typer.testing import CliRunner (same pattern as Click).
Subcommands?
app = typer.Typer() + sub = typer.Typer() + app.add_typer(sub, name="db").
Pydantic integration?
Use Pydantic models as types for structured config (Typer 0.12+).
How do I add help?
typer.Argument(help="...") or docstrings on commands.
Shell completion?
typer[all] includes completion. Install via --install-completion.
Async commands?
Use async def with typer 0.9+ async support or wrap with asyncio.run.
Multiple commands in one app?
Multiple @app.command() decorated functions.
How do I handle config files?
Load config separately; Typer handles CLI args. Consider pydantic-settings.
Is Typer production-ready?
Yes. Powers FastAPI CLI. Built on stable Click foundation.
Related
- Click - underlying framework
- argparse - stdlib option
- Config & Environment - settings
- CLI Best Practices - design rules
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.