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