Python CLI Tools (Typer)
Build polished CLIs with Typer, Rich output, and clean argument parsing.
# CLAUDE.md — Python CLI Tools (Typer)
## Project layout
```
src/yourcli/
__init__.py
__main__.py # python -m yourcli
cli.py # the Typer app
commands/
init.py
deploy.py
core/ # business logic, no Typer imports
pyproject.toml
```
- Entry point in `pyproject.toml`:
```toml
[project.scripts]
yourcli = "yourcli.cli:app"
```
- `cli.py` is thin — it parses input and calls into `core/`. All real work lives in `core/`.
## Typer basics
- One `Typer()` app per command tree. Sub-commands attach via `app.add_typer(sub_app, name="sub")`.
- Annotated types for parameters: `name: Annotated[str, typer.Option(help="...")]`. Use `typer.Argument` for positional, `typer.Option` for flags.
- Default values come from the parameter default, not from `typer.Option(default=...)`.
- Boolean flags get `--flag/--no-flag` automatically. Don't write your own.
## Output
- Use **Rich** for formatted output (tables, trees, syntax highlighting). It's already a Typer dep.
- Errors go to stderr (`typer.echo(msg, err=True)` or a `console = Console(stderr=True)`).
- Exit codes matter: `raise typer.Exit(code=1)` on failure. Don't `sys.exit` directly.
- Default to non-color when `stdout` isn't a TTY, or when `NO_COLOR` env var is set. Rich handles both automatically.
## Confirmation & prompts
- Destructive actions confirm: `typer.confirm("Proceed?", abort=True)`.
- Add `--yes` flags to skip confirmation in scripts. Default `False`.
- Don't prompt for required values silently — fail fast with a clear error if the flag is missing in non-interactive mode.
## Configuration
- Config order of precedence (highest to lowest): CLI flag → env var → config file → default.
- Use `pydantic-settings` for env-var/config loading. Validate types at the boundary.
- Config file location: `$XDG_CONFIG_HOME/<yourcli>/config.toml` (Linux/macOS), `%APPDATA%\<yourcli>\config.toml` (Windows). Use `platformdirs`.
## Logging
- `--verbose` / `-v` sets log level to DEBUG. `--quiet` / `-q` suppresses non-error output.
- Use `RichHandler` from `rich.logging`. It prints log entries pretty.
- Log file is optional; keep stdout clean for piping.
## Testing
- `from typer.testing import CliRunner`. Test commands with `runner.invoke(app, ["sub", "--flag"])`.
- Snapshot the output with `result.stdout` for golden-file tests.
- Don't import the CLI module inside `core/` tests — they should run without Typer.
## Don't
- Don't put business logic inside command functions. They're parsers, not orchestrators.
- Don't `print()` from library code that the CLI imports. Use `logging` and let the CLI configure handlers.
- Don't rely on the user's `cwd`. Take paths as arguments and resolve them explicitly.
- Don't add a flag for everything. Every flag is forever.
Other Python templates
Modern Python Rules
Type hints, ruff, black, uv, and pytest — opinionated Python defaults.
Python Data Science (pandas + numpy)
Notebook discipline, vectorization, and reproducible analysis with pandas.
Python Clean Architecture
Layered architecture with use-cases, repositories, and dependency inversion.
Python asyncio Patterns
asyncio fundamentals: tasks, gather, cancellation, and structured concurrency.
Django + DRF Rules
Django REST Framework conventions: viewsets, serializers, permissions.