python-package/skills/cli-architecture/SKILL.md
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 skillsauth add oborchers/fractional-cto cli-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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())tools
This skill should be used when the user invokes any /plan-* command from the planning-tools plugin (/plan-context, /plan-master, /plan-open-questions, /plan-verify, /plan-tick, /plan-progress, /plan-delete), asks how Claude Code's plan files work, asks where plans are stored, asks to author or audit a multi-phase master planning document, asks how to walk through a plan's Open Questions interactively, asks how to write progress entries, or mentions ~/.claude/plans/ or .claude/planning-tools.local.md. Provides the index of planning-tools commands, the master-plan workflow lifecycle, the v0.3.0+ list-shape mandate (phases and questions as headings + bulleted scope items, never tables), the v0.3.2+ plain-bullet shape (no `- [ ]` checkboxes — heading emoji is the sole tick signal), the progress-entry methodology, and the mechanics of Claude Code's plan-mode file storage.
testing
This skill should be used when the user is adjusting spacing, padding, margins, content density, section gaps, vertical rhythm, or separation between elements. Also applies when reviewing whether a design feels cramped or too sparse, choosing between borders and whitespace for separation, or defining a spacing system. Covers the 4px/8px spacing system, macro vs micro whitespace, content density spectrum, separation techniques (whitespace > background shifts > borders), and vertical rhythm.
development
This skill should be used when the user is defining brand personality in design, choosing between illustration and photography, adding motion or animation, creating visual motifs, ensuring layout variety, customizing CSS framework defaults, or calibrating the level of creative expression for a given context. Covers Lavie & Tractinsky's expressive aesthetics, the expression spectrum (restrained to bold), brand personality translation, illustration systems, photography direction, and template independence.
development
This skill should be used when the user is establishing visual importance, designing headings, creating focal points, designing CTAs or buttons, arranging label-data relationships, implementing scanning patterns (F-pattern, Z-pattern), or ensuring one dominant element per screen. Covers the three levers of hierarchy (size, weight, color), three-tier information architecture, the 'emphasize by de-emphasizing' principle, CTA design, and label-data relationships.