CLI cheatsheet. Pick by complexity.

uv add typer
import typer

app = typer.Typer()

@app.command()
def hello(name: str, count: int = 1):
    """Greet someone."""
    for _ in range(count):
        typer.echo(f"Hello {name}")

if __name__ == "__main__":
    app()
python script.py hello Alice --count 3
python script.py hello --help

Subcommands

@app.command()
def users(): ...

@app.command()
def posts(): ...
python script.py users
python script.py posts

Nested apps

users_app = typer.Typer()
app.add_typer(users_app, name="users")

@users_app.command()
def list_(): ...

@users_app.command()
def create(): ...
python script.py users list
python script.py users create

Typer options

@app.command()
def run(
    name: str = typer.Argument(...),         # required positional
    count: int = typer.Option(1, "--count", "-c", help="Repeat count"),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
    file: Path = typer.Option(None, "--file", "-f", exists=True),
):
    ...

Rich output

from rich import print
from rich.console import Console
from rich.table import Table

console = Console()
console.print("[bold red]Error![/bold red]")

table = Table(title="Users")
table.add_column("ID")
table.add_column("Name")
table.add_row("1", "Alice")
console.print(table)

rich integrates with Typer automatically.

Progress bars

from rich.progress import track

for item in track(items, description="Processing..."):
    process(item)

Prompts

name = typer.prompt("Your name")
confirmed = typer.confirm("Are you sure?")
password = typer.prompt("Password", hide_input=True)

Exit codes

import sys

if error:
    typer.echo("failed", err=True)
    raise typer.Exit(code=1)

Click (for power users)

uv add click
import click

@click.command()
@click.argument("name")
@click.option("--count", default=1, help="Repeat count")
@click.option("-v", "--verbose", is_flag=True)
def hello(name: str, count: int, verbose: bool):
    """Greet someone."""
    for _ in range(count):
        click.echo(f"Hello {name}")

if __name__ == "__main__":
    hello()
python script.py Alice --count 3 -v

Click groups

@click.group()
def cli(): pass

@cli.command()
def users(): ...

@cli.command()
def posts(): ...

if __name__ == "__main__":
    cli()

argparse (stdlib, no install)

import argparse

parser = argparse.ArgumentParser(description="My tool")
parser.add_argument("name")
parser.add_argument("--count", type=int, default=1)
parser.add_argument("-v", "--verbose", action="store_true")

args = parser.parse_args()

for _ in range(args.count):
    print(f"Hello {args.name}")

Subparsers

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)

users = subparsers.add_parser("users")
users.add_argument("--list", action="store_true")

posts = subparsers.add_parser("posts")

args = parser.parse_args()
match args.command:
    case "users": ...
    case "posts": ...

Configuration with pydantic-settings + CLI

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    database_url: str
    debug: bool = False
    
    model_config = SettingsConfigDict(cli_parse_args=True)

# python script.py --database-url=... --debug
settings = Settings()

For tools where config = CLI args + env + .env.

Auto-complete

# Typer
python script.py --install-completion

Generates shell completions automatically.

Distribution

[project.scripts]
mytool = "myapp.cli:app"

After uv pip install -e .: mytool ... available.

Decision

Use
Quick scriptsargparse (stdlib)
Modern CLIsTyper
Power featuresClick
Config-heavy toolspydantic-settings + Typer

For new CLIs: Typer is the modern default.

Common mistakes

  • argparse for complex hierarchies — code gets ugly. Use Typer.
  • No --help testing — make sure your CLI’s docs are readable.
  • Hard-coded paths — use Path types via Typer.
  • No exit codes — 0 for success, non-zero for failure.

Read this next

If you want my Typer + rich + pydantic-settings CLI starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .