CLI Best Practices
Rules for CLIs that are discoverable, scriptable, and respectful of user time.
How to Use This List
- Apply when designing new commands or reviewing CLI PRs
- Every rule supports either human usability or script automation
- Add
--helpexamples for every subcommand
A - Interface Design
- Single clear purpose. One tool, one job. Split unrelated functions.
- Subcommands for verbs.
myapp deploy, notmyapp --deploy. - Consistent flag names.
--verbose/-vacross all subcommands. - Meaningful help text. Every argument documented with example.
B - Scriptability
- Meaningful exit codes. 0 success, distinct codes for distinct failures.
- stdout for data, stderr for logs. Output is pipeable.
- --json or --plain flag. Machine-readable output option.
- Non-interactive mode.
--yes/--no-inputfor CI and scripts.
C - Configuration
- Flags override env vars override config file. Document precedence.
- Secrets via environment only. Never in config files in git.
- Sensible defaults. Common case works with zero flags.
- Validate early. Fail fast on bad config before work starts.
D - User Experience
- Progress for long operations. Spinner or progress bar (Rich/tqdm).
- Confirm destructive actions. Default No on delete/remove.
- Actionable error messages. What failed, why, and how to fix.
- Shell completion. Click/Typer completion installed and documented.
E - Distribution
- Entry point in pyproject.toml. Installable command name.
- pipx-compatible. Isolated install with minimal deps.
- Semver for CLI changes. Breaking flag changes bump MAJOR.
- README with quickstart. Install, first command, common examples.
FAQs
How many subcommands?
As many as needed, but each should be discoverable via --help.
Should CLIs be interactive by default?
No. Interactive only when explicitly requested or no args provided for wizard.
argparse or Click?
argparse for simple/zero-dep. Click/Typer for multi-command production tools.
How do I deprecate a flag?
Warning on use, document in help, remove in next major version.
Logging level flag?
-v once = INFO, twice = DEBUG. Log to stderr.
How do I handle passwords?
Env var or prompt with hidden input. Never argv flags.
CLI testing strategy?
CliRunner/typer testing for integration. Unit test core logic separately.
Version flag?
--version on every tool. Consistent across your CLI suite.
Color output?
Default auto. --no-color and NO_COLOR=1 support.
How long should --help be?
Concise per-flag. Link to docs for long examples.
Related
- CLI Basics - fundamentals
- argparse - stdlib parsing
- Packaging & Distributing CLIs - distribution
- Config & Environment - configuration
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.