C.W.K.
Stream
Lesson 03 of 04 · published

typer — Type Hints as the CLI Spec

~18 min · typer, type-hints, cli, modern

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

typer — click's type-hint-driven cousin

typer (from FastAPI's author) sits on top of click and uses Python type hints to declare arguments. def hello(name: str, count: int = 1) — name becomes a positional argument, count becomes an optional --count. The type hints drive everything; you don't repeat them in decorators. Pippa's CLI wrappers use typer.

The ergonomic win

The function signature IS the CLI spec. Type hints, defaults, type validations all come from there. name: str = typer.Option(...) for an explicit option. name: Annotated[str, typer.Option()] for richer config. The result is CLIs that look like normal Python functions, with all the static-type benefits.

Subcommands — easier than click

app = typer.Typer(), then decorate functions with @app.command(). Each function becomes a subcommand named after itself. app.add_typer(other_app, name="users") nests sub-typers for hierarchies. Modern Python projects often use typer over click for new CLIs.

Rich integration

typer integrates with the rich library by default — colored output, progress bars, tables, formatted exceptions. typer.echo for plain output, typer.secho for colored, typer.progressbar for progress. The polish is extensive without effort.

Pythonic Way: If you're writing a new CLI in 2026, default to typer. The type-hint-driven ergonomics are best-in-class. Reach for argparse only when zero-dependency matters; click when typer's abstractions don't fit. The three are siblings, not competitors — typer uses click under the hood.

Code

typer — type-hint driven·python
# pip install typer
import typer

app = typer.Typer()

@app.command()
def hello(name: str, count: int = 1, shout: bool = False):
    """Greet someone NAME times."""
    msg = f"Hello, {name}!"
    if shout:
        msg = msg.upper()
    for _ in range(count):
        typer.echo(msg)

if __name__ == "__main__":
    app()

# python tool.py alice --count 3 --shout
# HELLO, ALICE!
# HELLO, ALICE!
# HELLO, ALICE!
Subcommands with Typer·python
import typer

app = typer.Typer()

@app.command()
def add(name: str):
    """Add a user."""
    typer.echo(f"adding {name}")

@app.command()
def remove(name: str, force: bool = False):
    """Remove a user."""
    if not force:
        confirmed = typer.confirm(f"Remove {name}?")
        if not confirmed:
            raise typer.Abort()
    typer.echo(f"removed {name}")

if __name__ == "__main__":
    app()

# python tool.py add alice
# python tool.py remove bob --force
Annotated for richer option metadata·python
from typing import Annotated
import typer
from pathlib import Path

app = typer.Typer()

@app.command()
def process(
    input_path: Annotated[Path, typer.Argument(help="Input file path", exists=True)],
    output_path: Annotated[Path, typer.Argument(help="Output file path")],
    verbose: Annotated[bool, typer.Option("-v", "--verbose", help="Verbose output")] = False,
    workers: Annotated[int, typer.Option(min=1, max=64)] = 4,
):
    """Process input into output with N workers."""
    typer.echo(f"input: {input_path}")
    typer.echo(f"output: {output_path}")
    typer.echo(f"workers: {workers}")
    if verbose:
        typer.echo("verbose mode")

# Constraints (min/max/exists/etc.) checked automatically
Nested apps for big CLIs·python
import typer

main_app = typer.Typer()
users_app = typer.Typer()
reports_app = typer.Typer()

main_app.add_typer(users_app, name="users")
main_app.add_typer(reports_app, name="reports")

@users_app.command()
def add(name: str):
    typer.echo(f"users add {name}")

@users_app.command()
def list_all():
    typer.echo("all users")

@reports_app.command()
def generate(format: str = "json"):
    typer.echo(f"generating in {format}")

if __name__ == "__main__":
    main_app()

# python tool.py users add alice
# python tool.py users list-all          (kebab-case auto-generated)
# python tool.py reports generate --format csv

External links

Exercise

Rewrite the click CLI from the previous lesson's exercise as a typer CLI. The greet/farewell subcommands, with the same arguments. Use type hints to declare them. Compare the code length and clarity. (For bonus, add an option that uses Annotated[int, typer.Option(min=1, max=100)] and verify typer enforces the range.)

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.