A batteries-included template for building beautiful Python CLIs. Typer + Rich, layered config, plugin discovery, JSON output for scripting, tests, packaging, and CI. Clone, rename, ship.
Most Python CLIs start as a single argparse script and slowly accrete the same missing pieces:
config that layers env vars over a file, output that is pretty for humans and parseable for
scripts, a progress bar that does not mangle the terminal, friendly errors with real exit codes,
tests, and a way to pipx install a global command. This template ships all of that, wired
together and tested, so you delete what you do not need instead of building what you forgot.
It is deliberately small and readable. Every example command demonstrates one pattern that is easy to get wrong, and every pattern has a test you can copy.
flowchart TD
U[User runs mycli process --steps 5] --> T[Typer app]
T --> CB[Root callback]
CB --> CFG[Load layered Settings]
CFG --> CON[Configure console and logging]
CON --> ST[Attach AppState to context]
ST --> CMD[Subcommand runs]
CMD --> HUMAN[Rich table, panel or progress to stdout]
CMD --> MACHINE[Plain JSON to stdout for pipes]
CMD --> LOG[Leveled logs to stderr]
CMD --> ERR[Friendly error plus sysexits code]
Configuration resolves in strict precedence order. The first source that provides a value wins:
flowchart LR
A[Command line flags] --> B[Environment variables]
B --> C[TOML config file]
C --> D[Built-in defaults]
# 1. Clone the template
git clone https://github.com/AleBrito124356/python-cli-template.git
cd python-cli-template
# 2. Install with the dev tooling and register the git hooks
python -m pip install -e ".[dev]"
pre-commit install
# 3. Copy the environment example (optional; every setting has a default)
cp .env.example .env
# 4. Run it
mycli --help
mycli items --json | jq '.[].name'That is it. There is no API key required to run this template; it ships zero network calls.
The .env.example documents an optional MYCLI_API_KEY slot only for when your own commands
start calling a service. If you wire one up to NVIDIA NIM, the free
key starts with nvapi- and takes about two minutes to create.
Every command works at a terminal and in a pipe. Output sketches below are abbreviated.
A long task with a progress bar
$ mycli process --steps 50 --delay 0.01
Processing ----------------------------------- 50/50 0:00:00
+- Done ----------------------------------------+
| Processed 50 items across 4 workers. |
+-----------------------------------------------+A table for humans, JSON for scripts — the same command, one flag apart:
$ mycli items --kind service
Items
+----+---------+---------+--------+-------+
| id | name | kind | status | score |
|----+---------+---------+--------+-------|
| 1 | alpha | service | active | 91 |
| 3 | charlie | service | paused | 88 |
+----+---------+---------+--------+-------+
$ mycli items --json | jq -r '.[] | select(.status=="active") | .name'
alpha
bravo
...A Unix filter that reads stdin when piped:
$ echo "Hello World" | mycli transform --op slug
hello-world
$ mycli transform --op sort --input names.txtAn interactive wizard that also runs unattended:
$ mycli wizard # prompts for each field
$ mycli wizard --yes --name demo # accepts defaults, no prompts, CI-friendlyInspect configuration and its sources:
$ mycli config show # effective settings as a table, secrets redacted
$ mycli config path # plain path, safe to use in scripts
$ mycli --config ./mycli.toml -v items # file + verbose flag layered togetherList installed plugins:
$ mycli plugins listExit codes follow sysexits.h: 64 usage, 66 missing input, 70 internal error, 78 bad
config. Scripts can branch on them.
| Pattern | File | What to copy |
|---|---|---|
| Long task with a progress bar | commands/process.py |
track_task context manager, mid-run failure with exit code |
| Interactive wizard with a non-interactive fallback | commands/wizard.py |
Rich prompts plus a --yes escape hatch |
| Table output or JSON for scripting | commands/items.py |
--json/--table flag, config-driven default that a flag overrides |
| Reading from stdin when piped | commands/transform.py |
isatty detection, fail fast instead of hanging |
| Layered configuration | config.py |
pydantic-settings with a custom TOML source and a precedence chain |
| Themed console and JSON emitter | console.py |
one theme, Rich for humans, plain stdout for machines |
| Friendly errors and exit codes | errors.py |
fail helper, CliError hierarchy, sysexits constants |
| Leveled logging to stderr | logging.py |
Rich handler that keeps stdout clean for data |
| Entry-point plugin discovery | plugins.py + contrib/sample_plugin.py |
discover, normalize, mount, and a working sample |
| Global options and wiring | app.py |
root callback that builds state and attaches it to the context |
python-cli-template/
├── src/mycli/
│ ├── app.py # Typer app, global options, entry point
│ ├── config.py # layered Settings via pydantic-settings
│ ├── console.py # themed Rich console + JSON emitter
│ ├── logging.py # Rich logging handler, logs to stderr
│ ├── errors.py # friendly errors + sysexits exit codes
│ ├── plugins.py # entry-point plugin discovery
│ ├── commands/ # one example per hard-to-get-right pattern
│ │ ├── process.py # progress bar
│ │ ├── wizard.py # interactive + --yes
│ │ ├── items.py # table or --json
│ │ ├── transform.py # stdin filter
│ │ ├── config.py # config show / path
│ │ └── plugins.py # plugins list
│ └── contrib/sample_plugin.py # bundled plugin, template for your own
├── tests/ # CliRunner, config precedence, plugin loading
├── .github/workflows/ # ci.yml (lint+type+test, 3 versions), release.yml
├── pyproject.toml # deps, console script, ruff + mypy config
├── Makefile · .pre-commit-config.yaml · .env.example
Three steps turn mycli into your own tool. Say your command is acme:
- Rename the package directory and imports.
git mv src/mycli src/acme # update the internal package name and env prefix grep -rl "mycli" src tests pyproject.toml | xargs sed -i 's/mycli/acme/g' # then change env_prefix in src/acme/config.py from "MYCLI_" to "ACME_"
- Rename the console script and plugin group in
pyproject.toml:Update[project.scripts] acme = "acme.app:main" [project.entry-points."acme.plugins"] sample = "acme.contrib.sample_plugin:plugin"
PLUGIN_GROUPinsrc/acme/plugins.pyto"acme.plugins"to match. - Reinstall and verify.
python -m pip install -e ".[dev]" acme --help && pytest
Then delete the example commands you do not need and start adding your own. The scaffolding (config, console, errors, logging, plugins) is the part worth keeping.
- pipx (recommended for a global CLI).
pipx install .orpipx install acmeonce it is on PyPI. Each tool gets its own isolated environment and a command on the user'sPATH. - PyPI. Tag a release (
git tag v0.1.0 && git push --tags).release.ymlbuilds the sdist and wheel and runstwine check. The publish job is present but commented; enable it with PyPI Trusted Publishing (OIDC, no token to leak) or aPYPI_API_TOKENsecret. - Single binary. For users without Python, freeze the CLI:
shivbuilds a self-contained zipapp (shiv -c acme -o acme.pyz .), andPyInstallerproduces a native executable. Both work because the entry point is a plainmainfunction.
- Typer gives you argument parsing, help text, shell completion, and type-based validation from ordinary function signatures and type hints. It is built on Click, so it is battle-tested, and it stays out of the way. You write functions, not parser boilerplate.
- Rich handles the terminal: tables, progress bars, panels, prompts, syntax highlighting, and tracebacks, with automatic downgrade to plain text when output is piped or the terminal is dumb. That automatic downgrade is why this template can keep one code path for humans and machines.
Together they cover the two things a CLI is judged on: how it feels to type, and how it looks to read. Everything else here exists so those two never regress as the tool grows.
make dev # install with dev deps and git hooks
make lint # ruff
make type # mypy (strict)
make test # pytest
make cov # pytest with coverage
make build # sdist + wheel- python-automation-toolbox — 20 standalone Python automation scripts; a natural source of commands to drop into this template.
- ai-commit — a real-world CLI built on these exact patterns: writes commit messages and changelogs from your diff.
- pdf-power-tools — one CLI for everything PDF, plus a Python library; another example of the library-plus-CLI shape.
- data-cleaning-toolkit — profile, standardize, and validate messy CSVs from a Pandas library and a CLI.
MIT © 2026 Alejandro Brito. See LICENSE.