TODO: One-line description of what this CLI does and who it’s for. Change the title above.
- Primary use case: <e.g., bulk process files, manage deployments, query APIs>
- Works with: <e.g., local files, Git repos, Kubernetes, AWS, JSON logs>
- Status: <stable | beta | experimental>
- License: <MIT | Apache-2.0 | GPL-3.0 | Proprietary>
TODO: throughout this documentation ITALICS mark placeholder content that a new project would typically want to edit with its own information.
TODO: If you are starting a new project, there are lots of instructions and useful information in the "Appendix I: Using the poetrycli template" and Appendix II: Template Checklist sections.
poetrycli is a template for building modern Python CLI applications using:
- Python 3.12 or Python 3.13 or Python 3.14
- Poetry for packaging, dependency management, and
venvworkflow - Typer for CLI structure (commands, options, subcommands, help)
- Rich for consistent console output and pretty logging
- Ruff for formatting + linting
- MyPy (and Pyright/Pylance/typeguard) for strict type checking
- Pytest + coverage for tests
- pre-commit + GitHub Actions CI to keep everything enforced automatically
- dependabot + codeql to keep dependencies always up-to-date and security issues at bay
The poetrycli repo is intentionally opinionated because it was built to help the authors (2-space indentation, single quotes, strict typing, “select ALL rules” linting are examples) but includes escape hatches and TODO markers to customize quickly. Started in Jan/2026, by Daniel Balparda.
Since version 0.1.0 it is PyPI package: https://pypi.org/project/foobarnotreally/
TODO: change this header to match your project's conditions.
- poetrycli - Python/Poetry/Typer/Rich CLI Template
- Table of contents
- License
- Installation (TODO)
- Context / Problem Space (TODO)
- Design assumptions / Disclaimers (TODO)
- CLI Interface (TODO)
- Project Design (TODO)
- Development Instructions
- Security
- Reliability (TODO)
- Troubleshooting (TODO)
- FAQ (TODO)
- Glossary (TODO)
- Appendix I: Using the
poetryclitemplate - Appendix II: Template Checklist: turning
poetrycliinto your new CLI project in 12 steps- 0: Prerequisites (one-time per machine)
- 1: Decide your identity
- 2: Rename the Python package directory
- 3: Update
pyproject.toml(the big one) - 3.1 Required metadata
- 4: Sync the runtime
__version__ - 5: Update config app name
- 6: Customize the CLI banner / intro lines
- 7: Review lint policy (Ruff)
- 8: Keep tool versions aligned across files
- 9: Run the full validation suite (before first commit)
- 10: First release workflow (suggested)
- 11: Update README
- 12: “Cleanup”
Copyright 2025 Daniel Balparda balparda@github.com
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License here.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
This project includes or depends on third-party software. See:
- NOTICE <link> (if applicable)
- Dependency license list: <link or section>
- Contributions are accepted under: <same as project license | CLA | DCO>
- Policy: <link to CONTRIBUTING.md>
To use in your project just do:
pip3 install <your_pkg>and then from <your_pkg> import <your_library> (or other parts of the library) for using it.
- OS: <Linux | macOS | Windows>
- Architectures: <x86_64 | arm64>
- Minimum versions: <e.g., macOS 12+, Ubuntu 20.04+, Windows 11>
- python 3.12 - documentation
- rich 14.2+ - Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal - documentation
- typer 0.21+ - CLI parser - documentation
- platformdirs 4.5+ - Determines appropriate platform-specific dirs
- poetrycli - CLI app templates and utils
- TODO: add your main dependencies here too
<Describe the CLI in one paragraph. Emphasize outcomes and workflows.>
- Not intended for:
- Not a replacement for:
- A
- B
- stdin: <supported | not supported>
- Files: <paths, globs, formats>
- Network/API: <endpoints, services>
- Environment variables/config:
- stdout: <human output / structured output>
- stderr: <errors/logging>
- Files/artifacts:
- CLI flags/commands stability: <stable | may change>
- JSON output stability: <stable schema | best-effort>
- Backward compatibility:
- Environment: <filesystem, permissions, network access>
- Locale/encoding: <UTF-8 expected?>
- Time/timezone:
- Scale limits: <e.g., tested up to 10k files>
- Platform limitations: <e.g., Windows path edge cases>
- Edge cases: <symlinks, long paths, etc.>
- Deprecations are announced via:
- Timeline: <e.g., 2 minor versions>
- Migration guidance:
- Telemetry: <none | optional | on by default>
- What is collected:
- How to disable: <env var | config flag>
Minimal example.
<project> <command> <arg><project> <cmd> --flag value <input><project> <cmd> <input> --output <file>General shape:
<project> [global flags] <command> [command flags] [args]| Flag | Description | Default |
|---|---|---|
-h, --help |
Show help | <off> |
--version |
Show version and exit | <off> |
-v, -vv, -vvv, --verbose |
Verbosity (nothing=ERROR, -v=WARNING, -vv=INFO, -vvv=DEBUG) |
ERROR |
--color/--no-color |
Force enable/disable colored output (respects NO_COLOR env var if not provided) | --color |
| Command | Description |
|---|---|
<command1> |
|
< command2> |
|
help |
Show help for commands |
Command reference template
Use this template for each command:
<command>
Purpose:
Syntax
<project> <command> [flags] <arg1> [arg2...]
Arguments
- <arg1>:
- <arg2>:
Flags
| Flag | Type | Description | Default |
| --- | --- | --- | --- |
| --foo | string | | <default> |
| --bar | bool | | <false> |
Examples
<project> <command> <arg1>
<project> <command> <arg1> --foo value
Output
- Human mode:
- JSON mode (--json): \<top-level fields / schema link\>
- Files created:
Exit codes
- 0: Success
- 2: Usage error (invalid args)
- 3: Runtime error (network, filesystem, etc.)
- \<other\>:
Common errors
- \<error message\> →
- \<error message\> →- Linux:
~/.config/<project>/config.<json|yaml|toml> - macOS:
~/Library/Application Support/<project>/config.<...> - Windows:
%APPDATA%\<project>\config.<...>
# ~/.config/<project>/config.yaml
profile: default
timeout_ms: 30000
retries: 3
output:
format: human # or json
color: auto # auto|always|never<project> config validate
<project> config show --effective| Variable | Description | Default | Notes |
|---|---|---|---|
<PROJECT>_CONFIG |
Config file path | <auto> | |
<PROJECT>_LOG_LEVEL |
Log level | info | debug |
<PROJECT>_NO_COLOR |
Disable color | <unset> | obeys NO_COLOR too |
cat input.txt | <project> <command> --from-stdin- Human-readable (default)
- JSON (
--json) for automation and scripting
Rich can provide color output in logging and in CLI output. App:
- Respects
NO_COLORenvironment variable - Has
--no-color/--colorflag: if given will override theNO_COLORenvironment variable - If there is no environment variable and no flag is given, default to having color
To control color see Rich's markup conventions.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic failure |
| 2 | CLI usage error |
| 3 | Runtime dependency failure (network/filesystem) |
| 4 | Partial success (some items failed) |
Keep this stable if users will script against it.
- Log levels: error|warn|info|debug|trace
- Structured logs: <supported? --log-format=json>
- Debug bundle: <project> debug report (if available)
- Dry run:
--dry-run(no side effects) - Non-interactive:
--yes/--no-input - Force:
--force(document exactly what it bypasses)
<High-level description of components and how they interact.>
Example:
- CLI parser → configuration loader → core engine → output renderer
- Optional: plugins/adapters for external systems
| Component | Responsibility |
|---|---|
| cmd/ | CLI entrypoints and subcommands |
| internal/core/ | Core business logic |
| internal/io/ | Filesystem/network adapters |
| internal/output/ | Output formatting (human/JSON) |
- Parse args + load config
- Validate inputs
- Execute core operation(s)
- Collect results and render output
- Return exit code
- Clear actionable messages for user errors
- Structured errors for --json
- Avoid leaking secrets in errors/logs
- Principle of least privilege
- Secret handling: never log secrets; redact by default
- TLS verification: on by default; disabling requires explicit opt-in
- Intended scale:
- Complexity notes:
- Benchmarks:
.
├── CHANGELOG.md ⟸ latest changes/releases
├── LICENSE
├── Makefile
├── poetry.lock ⟸ this is maintained by Poetry, do not manually edit
├── pyproject.toml ⟸ most important configurations live here
├── README.md ⟸ this documentation
├── SECURITY.md ⟸ security policy
├── requirements.txt
├── .editorconfig
├── .gitignore
├── .pre-commit-config.yaml ⟸ pre-submit
├── .github/
│ ├── dependabot.yaml ⟸ Github dependency update pipeline
│ └── workflows/
│ ├── ci.yaml ⟸ Github CI pipeline
│ └── codeql.yaml ⟸ Github security scans and code quality pipeline
├── .vscode/
│ └── settings.json ⟸ VSCode configs
├── scripts/
│ └── template.py ⟸ Use template & directory for executable standalone scripts
├── src/
│ └── <your_pkg>/ ⟸ change this directory's name (originally mycli)
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py ⟸ Main CLI app entry point (Main())
│ ├── py.typed
│ ├── core/
│ │ ├── __init__.py
│ │ └── example.py ⟸ Business logic goes in this directory
│ ├── resources/
│ │ ├── __init__.py
│ │ └── config.py ⟸ Project resources/files go in this directory
│ └── utils/
│ ├── __init__.py
│ ├── logging.py ⟸ Useful modules go in this directory; Logging logic for example
│ └── template.py ⟸ Use template for starting regular modules
├── tests/
│ └── test_cli.py ⟸ Unit-Testing goes in this directory
└── tests_integration/
└── test_installed_cli.py ⟸ Integration testing goes in this directoryWhat each area is for:
src/<your_pkg>/cli.py: Typer app definition, top-level callback (Main), and all commands/subcommands.src/<your_pkg>/core/example.py: “Business logic” layer. CLI commands call into here. This is the main testable logic layer.src/<your_pkg>/utils/template.py: A template module showing a recommended docstring structure for new modules.src/<your_pkg>/utils/logging.py: Rich-based logging config + a Console singleton for consistent output everywhere.src/<your_pkg>/resources/config.py: “Where is my config file?” logic using platformdirs.tests/test_cli.py: Comprehensive CLI tests using Typer’s CliRunner, pytest.mark.parametrize, and unittest.mock.patch.scripts/template.py: A template for “directly executable scripts” (includes a shebang).
Specifically note and use the templates.
src/<your_pkg>/utils/template.pyis a suggested “module docstring skeleton” (purpose, API, inputs, errors, security, etc.). Copy it when creating new modules.scripts/template.pyis a suggested “executable script skeleton” (with shebang) that imports and calls into the package. Scripts should remain thin.
Make sure you are familiar with the poetrycli Features explained for this project so you understand the philosophy behind developing for the structure here.
Here is a suggested recipe to install an arbitrary Python version on Linux:
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install git python3 python3-dev python3-venv build-essential software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.12 # or python3.13 or python3.14 - TODO: pick a versionand on Mac:
brew update
brew upgrade
brew cleanup -s
brew install git python@3.12 # or python3.13 or python3.14 - TODO: pick a versionInstall pipx (if you don’t have it):
python3 -m pip install --user pipx
python3 -m pipx ensurepathIf you previously had Poetry installed, but not through pipx make sure to remove it first: brew uninstall poetry (mac) / sudo apt-get remove python3-poetry (linux). You should install Poetry with pipx and configure poetry to create .venv/ locally. This keeps Poetry isolated from project virtual environments and python for the environments is isolated from python for Poetry. Do:
pipx install poetry
poetry --versionIf you will use PyPI to publish:
poetry config pypi-token.pypi <TOKEN> # add your personal PyPI project token, if anyThis template expects a project-local virtual environment at ./.venv (VSCode settings assume it, for example).
poetry config virtualenvs.in-project truegit clone https://github.com/balparda/poetrycli.git poetrycli # TODO: change to your project's repo
cd poetrycliFrom the repository root:
poetry env use python3.12 # creates the .venv with the correct Python version - TODO: pick correct Python version
poetry sync # sync env to project's poetry.lock file
poetry env info # no-op: just to check that environment looks good
poetry check # no-op: make sure all pyproject.toml fields are being used correctly
poetry run mycli --help # simple test if everything loaded OK
make ci # should pass OK on clean repoTo activate and use the environment do:
poetry env activate # (optional) will print activation command for environment, but you can just use:
source .venv/bin/activate # because .venv SHOULD BE LOCAL
...
pytest -vvv # for example, or other commands you want to execute in-environment
...
deactivate # to close environmentThis repo ships a .vscode/settings.json configured to:
- use
./.venv/bin/python - run
pytest - use Ruff as formatter
- disable deprecated pylint/flake8 integrations
- configure Google-style docstrings via autoDocstring
- use Code Spell Checker
Recommended VSCode extensions:
- Python (
ms-python.python) - Python Environments (
ms-python.vscode-python-envs) - Python Debugger (
ms-python.debugpy) - Pylance (
ms-python.vscode-pylance) - Mypy Type Checker (
ms-python.mypy-type-checker) - Ruff (
charliermarsh.ruff) - autoDocstring – Python Docstring Generator (
njpwerner.autodocstring) - Code Spell Checker (
streetsidesoftware.code-spell-checker) - markdownlint (
davidanson.vscode-markdownlint) - Markdown All in One (
yzhang.markdown-all-in-one) - helps maintain thisREADME.mdtable of contents - Markdown Preview Enhanced (
shd101wyy.markdown-preview-enhanced, optional)
<build command><project> --help
<run-from-source command>make test # plain test run, no integration tests
make integration # run the integration tests
poetry run pytest -vvv # verbose test run, includes integration tests
make cov # coverage run, equivalent to: poetry run pytest --cov=src --cov-report=term-missingA test can be marked with a "tag" by just adding a decorator:
@pytest.mark.slow
def test_foo_method() -> None:
"""Test."""
...These tags, like slow above are defined in pyproject.toml, in section [tool.pytest.ini_options.markers], and you can define your own there. The ones already defined are:
| Tag | Meaning |
|---|---|
slow |
test is slow (> 1s) |
flaky |
AVOID! - test is known to be flaky |
stochastic |
test is capable of failing (even if very unlikely) |
You can use them to filter tests, for example:
poetry run pytest -vvv -m slow # run only the slow testsYou can find the slowest tests by running (example suggestions):
poetry run pytest -vvv -q --durations=20
poetry run pytest -vvv -q --durations=20 -m "not slow" # find unknown slow methods
poetry run pytest -vvv -q --durations=20 -m slow # check methods marked `slow` are in fact slowYou can search for flaky tests by running make flakes, which runs all tests 100 times. Or you can do more, like in the example:
make flakes # equivalent to: poetry run pytest --flake-finder --flake-runs=100 -q tests
poetry run pytest --flake-finder --flake-runs=10000 -m "not slow"You can instrument your code to find bottlenecks:
$ source .venv/bin/activate
$ which mycli
/path/to/.venv/bin/mycli # <== place this in the command below:
$ pyinstrument -r html -o output1.html -- /path/to/.venv/bin/mycli <your-cli-command> <your-cli-flags>
$ deactivateThis will save a file output1.html to the project directory with the timings for all method calls. Make sure to cleanup these html files later.
There are integration tests that build a pip wheel and test the CLI.
make integrationThey are slower, but are a part of the CL pipeline.
- Human output:
- JSON output:
make lint # equivalent to: poetry run ruff check .
make fmt # equivalent to: poetry run ruff format .To check formatting without rewriting:
poetry run ruff format --check .make type # equivalent to: poetry run mypy src(Pyright is primarily for editor-time; MyPy is what CI enforces.)
- How docs are built: <mkdocs/docusaurus/sphinx/etc.>
- CLI reference generation:
Make sure you are familiar with the poetrycli Features explained for this project so you understand the philosophy behind developing for the structure here.
This project follows a pragmatic versioning approach:
- Patch: bug fixes / docs / small improvements.
- Minor: new template features or non-breaking developer workflow changes.
- Major: breaking template changes (e.g., required file/command renames).
See: CHANGELOG.md
Poetry can bump versions:
# bump the version!
poetry version minor # updates 1.6 to 1.7, for example
# or:
poetry version patch # updates 1.6 to 1.6.1
# or:
poetry version <version-number>
# (also updates `pyproject.toml` and `poetry.lock`)This updates [project].version in pyproject.toml. Remember to also update src/<your_pkg>/__init__.py to match (this repo gets/prints __version__ from there)!
The project has a dependabot config file in .github/dependabot.yaml that weekly (defaulting to Tuesdays) scans both Github actions and the project dependencies and creates PRs to update them.
To update poetry.lock file to more current versions do poetry update, it will ignore the current lock, update, and rewrite the poetry.lock file. If you have cache problems poetry cache clear PyPI --all will clean it.
To add a new dependency you should do:
poetry add "pkg>=1.2.3" # regenerates lock, updates env (adds dep to prod code)
poetry add -G dev "pkg>=1.2.3" # adds dep to dev code ("group" dev)
# also remember: "pkg@^1.2.3" = latest 1.* ; "pkg@~1.2.3" = latest 1.2.* ; "pkg@1.2.3" exactKeep tool versions aligned. This repo pins:
ruffandmypyversions inpyproject.toml- and also pins them in
.pre-commit-config.yaml
If you bump one, bump the other (otherwise you’ll get “works in CI/IDE but fails in pre-commit” mismatches). Remember to check your diffs before submitting (especially poetry.lock) to avoid surprises!
This template does not generate requirements.txt automatically (Poetry uses poetry.lock). If you need a requirements.txt for Docker/legacy tooling, use Poetry’s export plugin (poetry-plugin-export) by simply running:
poetry export --format requirements.txt --without-hashes --output requirements.txtTip: If you want auto-export every time the lockfile changes, consider a plugin like poetry-auto-export (optional policy choice).
Publish to GIT, including a TAG:
git commit -a -m "release version 0.1.0"
git tag 0.1.0
git push
git push --tagsIf you already have your PyPI token registered with Poetry (see Install Poetry) then just:
poetry build
poetry publishRemember to update CHANGELOG.md.
- *See
CONTRIBUTING.md* - Code of conduct:
CODE_OF_CONDUCT.md
Please refer to the security policy in SECURITY.md for supported versions and how to report vulnerabilities.
The project has a codeql config file in .github/workflows/codeql.yaml that weekly (defaulting to Fridays) scans the project for code quality and security issues. It will also run on all commits. Github security issues will be opened in the project if anything is found.
- Dependency pinning:
- Signed releases: <GPG/cosign>
- SBOM: <available? where?>
- Recommended timeouts:
- Retry behavior:
- Idempotency:
- CI usage examples
- Cron usage examples
- Non-interactive flags (
--yes,--json,--quiet)
- Network failures:
- Partial failures: <exit code + output behavior>
- Rate limiting:
<project> --verbose
<project> --log-level debug <command>-
Problem: Cause: Fix:
-
Problem: Fix:
<project> debug report --output diagnostics.zip<Answer>
<Answer + link to migration guide>
<Answer + schema contract>
- A
- B
If you are starting a new CLI app based on poetrycli, then these are the steps to follow. Know that if you just used the poetrycli template to start a new repository then your files in this repo include many # TODO: markers to guide you on where to change the files.
(Note there is also an Appendix II: Template Checklist)
The suggested instructions to start with is:
-
Rename the package directory:
src/mycli/→src/<your_pkg>/ -
Update the script entrypoint in
pyproject.toml:
[tool.poetry.scripts]
mycli = "mycli.cli:app" → <yourcli> = "<your_pkg>.cli:app"- Update Poetry packaging (change
myclito your package name):
[tool.poetry]
packages = [{ include = "<your_pkg>", from = "src" }]- Update the project metadata:
[project]
name = "<your_pkg>" → your PyPI/project name
also change version, description, authors, classifiers
[project.urls]
change GitHub URLs and others- Update
src/<your_pkg>/__init__.py: keep__version__ = "..."in sync with[project].version.
In src/<your_pkg>/resources/config.py:
APP_NAME = '<your_pkg>' # TODO: change this to your app nameChange APP_NAME to your app name so config ends up under the correct OS-specific directory.
This template currently targets Python 3.12 (or Python 3.13 or Python 3.14). It may possibly work with more versions, but these ones the authors have tested. If you want a different Python version, update the “Python version cluster” in multiple places, at least update all of these:
pyproject.toml:- [project.classifiers] (e.g., "Programming Language :: Python :: 3.12")
- [project.requires-python] (e.g., ">=3.12")
- [tool.poetry.dependencies].python (e.g., "^3.12")
- [tool.ruff].target-version (e.g., "py312")
- [tool.mypy].python_version (e.g., "3.12")
- [tool.pyright].pythonVersion (e.g., "3.12")
.github/workflows/ci.yaml: matrixpython-versionREADME.md(this file): Python version references in install instructions
After changing versions, re-create your .venv (if you have already created it):
rm -rf .venv
make # equivalent to: poetry installIn src/<your_pkg>/cli.py, Main() prints a banner and logs an example warning:
- Replace banner text (
“<your_pkg>”, email, etc.) - Remove example options you don’t want (
--foo,--bar) or rename them into real app options
Ruff rule reference. This template currently uses:
[tool.ruff.lint]
select = ["ALL"]
ignore = [...a few specific ones...]If that’s too strict for your team, you can:
- Keep
ALLand expand theignore = [...]list, or - Remove
ALLand select only the groups you want that come commented out by default.
This documents how to use poetrycli-derived projects. Some things here are a result of how this project is organized and meant to be used. Others may be good ideas regardless.
Before continuing it makes sense to make sure you are familiar with the Development Instructions and have gone over the Development Setup and understand the File structure of the project.
The CLI is defined as a Typer application object:
app = typer.Typer(add_completion=True, no_args_is_help=True)A single callback works as the global “constructor”:
@app.callback(invoke_without_command=True)supports out-of-the-box:
--versionflag (prints version and exits)-v/--verbose(verbosity counter)- example options
--fooand--bar
Verbosity is an integer counter:
- no
-v: verbosity = 0 →ERRORlevel logging -v: verbosity = 1 →WARNINGlevel logging-vv: verbosity = 2 →INFOlevel logging-vvv: verbosity >= 3 →DEBUGlevel logging
The callback calls:
console = cli_logging.InitLogging(verbose)That configures logging and installs a shared Rich console singleton. Commands included out-of-the-box:
poetry run <yourcli> config-path→ prints the config file pathpoetry run <yourcli> hello [name]→ printsHello, <name>!
Subcommand example group included:
poetry run <yourcli> random num --min 0 --max 100poetry run <yourcli> random str --length 16 [--alphabet ...]
This is implemented via:
_random_app = typer.Typer(no_args_is_help=True)
app.add_typer(_random_app, name='random', help='Random utilities.')This is a key opinionated feature of the template. Console() returns the global singleton if initialized, otherwise returns a fallback rich.console.Console(). This allows any command do:
console = cli_logging.Console()
console.print(...)without worrying whether logging was initialized, which should be done only once:
InitLogging(verbosity, include_process=False, soft_wrap=False)this:
- creates a Rich
Console(soft_wrap=...) - configures Python logging with
RichHandler - sets logging level based on verbosity
- sets
force=Trueinlogging.basicConfig(...)to override prior config - normalizes logging for “common providers” (uvicorn/gunicorn/etc.) to propagate into your handler
- logs a startup info line: Logging initialized at level ...
Testing note: For tests that rely on fresh logging init, call ResetConsole() in an autouse fixture (there is an example in tests/test_cli.py). This prevents cross-test leakage of the singleton.
Commands call into src/<your_pkg>/core/example.py, which should contain your business logic. Why this pattern is useful:
- CLI remains thin and testable
- Business logic can be tested independently (and reused elsewhere)
- Mocking business logic is cleaner
This is minimal and cross-platform.
GetConfigDir()usesplatformdirs.user_config_path(APP_NAME)GetConfigPath()returnsGetConfigDir() / "config.toml"
So your config location becomes OS-native:
- macOS:
~/Library/Application Support/<APP_NAME>/... - Linux:
~/.config/<APP_NAME>/... - Windows: user
AppDataequivalent
This template uses Ruff for both: ruff check (lint) and ruff format (format). Key formatting opinions:
indent-width = 2
quote-style = "single"
docstring-code-format = trueLint configuration by default selects ALL rules, then ignores specific rules that conflict with this template’s choices. Notable ignores:
N802: allow PascalCase for function/method namesE111,E114: allow 2-space indentation- formatter-conflict ignores:
D203,D213,COM812 TC002: allow “third-party import only used for typing” patterns- a few practical exceptions for CLI ergonomics and TODO policy
If you want a calmer baseline, remove "ALL" and explicitly select rule groups.
This repo supports strict typing in three ways:
- MyPy: configured via
[tool.mypy]inpyproject.toml(strict = true, plus many explicit strict flags) - Pyright/Pylance: configured via
[tool.pyright]inpyproject.toml(typeCheckingMode = "strict") - typeguard: configured in
[tool.pytest.ini_options.typeguard-*]inpyproject.toml
VSCode uses Pylance by default, so you get IDE-time feedback and CI-time enforcement. typeguard will be active during tests by default. You can suppress type checking in specific tests by invoking @typeguard.suppress_type_checks decorator or context:
import typeguard
@typeguard.suppress_type_checks
def test_crazy_types() -> None:
# whole method is exempt from typeguard
def test_less_crazy_types_test() -> None:
# this part of test is type-checked
with typeguard.suppress_type_checks():
# this part is not type checkedTests run with pytest, and CI runs coverage:
make cov # equivalent to: poetry run pytest --cov=src --cov-report=term-missingCoverage configuration in pyproject.toml omits:
*/__init__.py*/__main__.py*/template.py
Rationale: these files should remain “thin” and are usually not meaningful coverage targets.
File .pre-commit-config.yaml defines hooks for Ruff lint (ruff-check), Ruff format (ruff-format), and MyPy (mypy). Install hooks:
poetry run pre-commit installRun on all files:
make precommit # equivalent to: poetry run pre-commit run --all-filesImportant: pre-commit runs tools in its own isolated environments pinned by rev: so if you bump Ruff/MyPy versions in pyproject.toml remember to update .pre-commit-config.yaml too.
File .github/workflows/ci.yaml runs on pushes and PRs:
poetry installruff check .ruff format --check .mypy srcpytest --cov=src --cov-report=term-missing
CI is the “source of truth” that the template remains clean.
This checklist is designed to be mechanical: do every step, run the commands, and you’ll end up with a renamed, clean, working project.
- Install Python 3.12 (or your chosen target version).
- Install Poetry via
pipx:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install poetry- Configure Poetry to put the virtualenv in-project:
poetry config virtualenvs.in-project trueChoose:
- project name (PyPI name): e.g.
coolcli - package name (import name): e.g.
coolcli(usually same as project name) - CLI command name: e.g.
cool - app config name: e.g.
coolcli(used for config directories)
-
Rename the package folder:
src/mycli/→src/<your_pkg>/ -
Search/replace imports:
from mycli ...→from <your_pkg> ...import mycli ...→import <your_pkg> ...
-
Update tests that import the CLI app (e.g.
tests/test_cli.py).
Open pyproject.toml and update all the # TODO: markers.
Under [project]:
name = "mycli"→ your project nameversion = "0.1.0"→ your initial versiondescription = ...authors = [...]license = ...classifiers(especially the Python version classifier)requires-python = ">=3.12"(or your chosen version)license-files = ["LICENSE"](keep unless you change licensing docs)
Under [project.urls]:
- Homepage/Repository/Issues/Changelog → update to your repo URLs
- Under
[tool.poetry]:packages = [{ include = "mycli", from = "src" }]→ updateincludeto your package name - Under
[tool.poetry.scripts]:mycli = "mycli.cli:app"→ update both sides:- CLI command name (left)
- import path (right) to match your renamed package
- In
.github/workflows/ci.yamlupdatemycliname intypeguard-packages
There are at least six places to update (the file literally warns you). Update all of:
[project.classifiers]→Programming Language :: Python :: 3.xx[project.requires-python][tool.poetry.dependencies] python = ...[tool.ruff] target-version = "py3xx"[tool.mypy] python_version = "3.xx"[tool.pyright] pythonVersion = "3.xx"
Also update:
.github/workflows/ci.yamlpython matrix version.- README references
The CLI prints __version__ from your package. Update:
src/<your_pkg>/__init__.py→__version__ = "<same as pyproject.toml>"
Update tests that assert version output (there is a version test).
In src/<your_pkg>/resources/config.py: APP_NAME = 'mycli' → your app name
This affects where the OS-native config directory lives.
In src/<your_pkg>/cli.py, update:
- the “MYCLI” banner text
- the email/name line
- remove example “foo/bar” options if you don’t want them (or repurpose them).
This template uses select = ["ALL"] and then ignores a curated list. Decide:
- Keep
ALL(recommended if you like strictness), or - Replace
ALLwith a smallerselectlist.
If you want to keep PascalCase methods:
- Ensure
N802remains ignored (that’s the “function name should be lowercase” complaint).
pyproject.tomldev deps (ruff/mypy/pytest etc.).pre-commit-config.yamlrev pins + mypyadditional_dependenciespins
Rule of thumb:
- If you bump ruff or mypy in
pyproject.toml, bump their pre-commitrevtoo. - If you bump runtime deps like rich/typer/pytest, update mypy hook’s
additional_dependenciespins.
From repo root:
make # equivalent to: poetry install
make ci # runs complete CI pipelineExpected:
- Ruff: no diffs after
ruff format . - Ruff lint: clean
- MyPy: clean
- Pytest: green
- Coverage: acceptable (note: init/template files are omitted by design) 
- Ensure version is correct: confirm in
pyproject.toml+__init__.py - Run the full validation suite (Step 9)
- Commit, tag, and publish per your release process
- Delete this "Appendix II" from the docs
- You probably want to at least partially keep the rest of the documentation
- Look for TODOs and ITALICS for places to edit
- Not all topics and sections are relevant for every project: pick the ones you want, and maybe delete the rest
- rename “mycli” references, usage examples, repo links
CHANGELOG.md(reset it to your new project)SECURITY.md(make sure contact details are up to date)LICENSEfile and README header if your project changes license/ownership
Once your project is real:
- Remove or repurpose the example commands (Hello, random, etc.)
- Remove
scripts/template.pyif you don’t use direct executable scripts - Remove
src/<pkg>/utils/template.pyonce everyone knows the pattern - Tighten or relax Ruff ignores based on your team’s preferences
Thanks! - Daniel Balparda