From python-package
Guides CLI architecture for Python packages: framework selection (Click, Typer, argparse), layouts (cli.py/cli/ dir), __main__.py delegation, entry points, exit codes, subcommands.
How this skill is triggered — by the user, by Claude, or both
Slash command
/python-package:cli-architectureThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Not every package needs a CLI, but when it does, the structure and framework choice have long-term consequences for maintenance, discoverability, and user experience. The Python ecosystem has converged on clear patterns -- 7 out of 13 surveyed top packages (httpx, Black, Hatch, Flask, cookiecutter, rich-cli, Typer itself) use Click directly. Typer wraps Click and is gaining adoption for simpler...
Not every package needs a CLI, but when it does, the structure and framework choice have long-term consequences for maintenance, discoverability, and user experience. The Python ecosystem has converged on clear patterns -- 7 out of 13 surveyed top packages (httpx, Black, Hatch, Flask, cookiecutter, rich-cli, Typer itself) use Click directly. Typer wraps Click and is gaining adoption for simpler CLIs. argparse remains the choice when zero external dependencies is a hard requirement.
Choose the CLI framework based on your constraints and complexity:
| Your CLI | Use This Framework | Why |
|---|---|---|
| Zero dependencies required | argparse (stdlib) | No external deps; used by pytest, pre-commit, Django |
| Simple-to-moderate, modern Python (3.10+) | Typer | Less boilerplate, leverages type hints, Click underneath |
| Complex with subcommand groups, plugins, custom types | Click | Battle-tested, explicit decorators, deep customization |
| Performance-critical tool | Clap (Rust) + maturin | Used by ruff, uv; not a Python CLI at all |
What top packages use:
| Package | Framework | Reason |
|---|---|---|
| Flask, Hatch, Black, httpx, cookiecutter | Click | Complex CLIs, plugin systems, mature ecosystem |
| fastapi-cli | Typer | Same author, type-hint-driven API |
| pytest, pre-commit | argparse | Zero-dependency policy |
| ruff, uv | Clap (Rust) | Performance-critical, not Python CLIs |
Click has ~530M monthly downloads; Typer has ~100M and growing. Typer IS Click underneath -- choosing Typer means you are using Click with a type-hint-based API surface. The Python Packaging User Guide now demonstrates Typer as its primary CLI example.
Two patterns, chosen by CLI complexity:
Single cli.py file (small-to-medium CLIs, used by Flask, httpx, pre-commit, fastapi-cli):
src/my_package/
__init__.py
__main__.py # delegates to cli.main()
cli.py # all CLI logic here
cli/ directory with subcommand modules (large CLIs, used by Hatch, pip, poetry):
src/my_package/
__init__.py
__main__.py # delegates to cli.main()
cli/
__init__.py # main group, imports subcommands
build.py # 'build' subcommand
publish.py # 'publish' subcommand
env/
__init__.py # 'env' subcommand group
create.py
remove.py
Start with cli.py. Migrate to cli/ when subcommands exceed 3-4 or when individual subcommands are complex enough to warrant their own modules.
__main__.py for python -m SupportEvery package with a CLI should provide __main__.py so users can run python -m my_package. The file purely delegates -- it never contains CLI logic.
"""Allow running as: python -m my_package"""
from my_package.cli import main
raise SystemExit(main())
Key conventions:
raise SystemExit(main()) instead of sys.exit(main()) -- avoids importing sys and works the same waymain() function must return an integer exit code (0 = success, 1 = error, 2 = usage error)[project.scripts] and __main__.py must call the same function -- users expect identical behavior from my-cli and python -m my_packageRegister the CLI via [project.scripts] in pyproject.toml (see pyproject-toml skill for the full entry points section):
[project.scripts]
my-cli = "my_package.cli:main"
The entry point and __main__.py point to the same main() function:
| Invocation | Mechanism | Calls |
|---|---|---|
my-cli | [project.scripts] entry point | my_package.cli:main() |
python -m my_package | __main__.py | my_package.cli:main() |
Click-based packages: Click handles exit codes automatically in standalone_mode=True (the default). ctx.exit(code) or raising click.exceptions.Exit(code) propagates to sys.exit(). Exit code 0 = success, 2 = usage error (bad arguments).
Typer-based packages: Uses raise typer.Exit(code=N) which delegates to Click's exit mechanism underneath.
argparse-based packages: Return an integer from main(), and __main__.py calls raise SystemExit(main()).
All frameworks follow the same convention:
| Exit Code | Meaning | When |
|---|---|---|
| 0 | Success | Command completed normally |
| 1 | Error | Runtime failure (network error, file not found, validation failure) |
| 2 | Usage error | Bad arguments, missing required options |
| Anti-Pattern | Consequence |
|---|---|
CLI logic in __main__.py | Cannot be imported or tested independently |
sys.exit() scattered throughout CLI code | Hard to test, bypasses cleanup |
[project.scripts] and __main__.py calling different functions | Users get different behavior depending on invocation |
Missing __main__.py | python -m package does not work |
| Mixing Click and argparse in the same package | Inconsistent argument parsing, confusing error messages |
When reviewing a Python package CLI:
cli.py or cli/ directory, not in __init__.py or __main__.py__main__.py exists and purely delegates to cli.main()[project.scripts] and __main__.py call the same functionmain() returns an integer exit code__main__.py uses raise SystemExit(main()), not sys.exit(main())npx claudepluginhub oborchers/fractional-cto --plugin python-packageBuilds command-line interfaces for Python libraries using Click or Typer. Covers framework selection, entry points, progress bars, colored output, shell completion, and testing with CliRunner.
Application and library conventions for plain Python projects (no web framework): src/ package layout, CLI tooling (argparse/click/typer), configuration management (pydantic-settings), logging setup, module design, entry points, and packaging conventions. Activated automatically by python-plugin/stack.md as a convention skill for the development phase. Use this skill to: - Organise Python application code in a src/ layout with proper package structure. - Build CLI tools with argparse, click, or typer and register them as console scripts. - Manage configuration from environment variables using pydantic-settings. - Set up structured logging for production-grade applications. - Package a Python project correctly with pyproject.toml. Do NOT use this skill for: - Language idioms (type hints, dataclasses, enums, match/case) — see python-foundation:python-conventions. - Package manager commands (ruff, mypy, pip/poetry/uv) — see python-foundation:python-tooling. - Testing patterns — see python-foundation:pytest-testing. - Web framework patterns (Django/FastAPI/Flask) — see those framework plugin skills.
Builds CLI tools with argument parsing, interactive prompts, progress indicators, and shell completions across Node.js, Python, and Go ecosystems.