From python-package
This skill should be used when the user is adding a CLI to a Python package, choosing between Click, Typer, and argparse, structuring cli.py or a cli/ directory, creating a __main__.py for python -m support, defining console script entry points, handling exit codes, or organizing subcommands. Covers framework selection, CLI module layout, __main__.py delegation pattern, exit code conventions, and subcommand organization.
npx claudepluginhub oborchers/fractional-cto --plugin python-packageThis skill uses the workspace's default tool permissions.
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...
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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())