Python
Purpose
Provide portable Python defaults that emphasize explicit typing, simple structure, maintainable CLIs, and focused tests.
When to use this skill
- Writing or refactoring Python application code.
- Designing a Python CLI or maintenance script.
- Choosing how to type shared data and interfaces.
- Deciding where tests should live and what they should cover.
- Reviewing Python code for readability and long-term maintainability.
Scope Boundaries
- Use this skill for portable Python structure, typing, CLI, and testing guidance.
- Use
ref-sp-dev-coding-patterns for language-agnostic naming, comment, and CLI ergonomics defaults.
- Use
ref-sp-dev-projects-architecture for shared-utility thresholds and product-versus-maintenance boundaries.
- Use a repo's own repo-conventions skill (in this repo,
ref-sp-dev-repo-conventions) when the question is about that repository's exact package names, top-level folders, or validation commands.
Defaults
- Prefer modern Python with type hints throughout public and shared code.
- If the repo already targets a modern Python baseline such as 3.14+, do not add
from __future__ import annotations or similar compatibility boilerplate just to mimic older code.
- Prefer inferred return types for local helpers when the type checker can infer them cleanly; add return annotations when the function defines an API contract or inference would hide ambiguity.
- Prefer
pathlib.Path over raw path strings.
- Prefer dataclasses, typed dicts, or small domain objects over loose dictionaries when structure matters.
- Prefer explicit exceptions and clear error messages over silent fallbacks.
- Prefer inert module imports: defer connections, I/O, and client construction to factory functions or lazy accessors rather than running them at module scope.
- Prefer
uv for Python dependency management, virtual environments, and runnable project commands unless the repo already mandates another Python workflow.
- In
uv-managed repos that use Poe, prefer tasks that invoke installed console entry points through uv run instead of adding tiny wrapper scripts.
- Prefer the repo's standard formatter, type checker, and test task wrappers when they exist.
Task Framing
| Command or action |
What |
Why |
When |
Expected outcome |
| Organize a Python feature |
Choose a feature folder, local modules, and collocated tests. |
A good starting layout keeps future refactors local instead of repo-wide. |
When adding a new unit of behavior. |
The feature is easy to find, extend, and test. |
| Decide between product CLI and maintenance script |
Choose whether a command belongs under the package or in repo maintenance paths. |
Many Python repos accumulate product behavior in ad hoc scripts. |
When a new command-line flow appears. |
Product commands are packaged cleanly and maintenance glue stays separate. |
| Review types and tests together |
Check whether the public API, data structures, and risky branches are explicit. |
Python stays maintainable when type clarity and test coverage grow together. |
When reviewing or refactoring non-trivial logic. |
Data shapes are clear and the fragile branches are covered. |
Core Rules
Typing
- Type function parameters clearly.
- Prefer inferred return types for private/local helpers whose implementation makes the result obvious to the checker and reader.
- Add return annotations for public APIs, shared protocol or callback contracts, abstract methods, recursive functions, overload-style dispatch, CLI entrypoints, and cases where inference would become
Any, object, or an overly broad union.
- On modern Python baselines, use standard annotation syntax directly instead of future-compatibility imports for postponed annotations.
- Prefer precise container types like
list[str] or dict[str, int].
- Prefer
object plus narrowing, focused casts, or type guards at unknown input boundaries instead of defaulting to Any.
- Reserve
Any for rare interoperability gaps that cannot be expressed cleanly with narrower types.
- Use
Protocol, TypedDict, dataclasses, or type aliases when they improve readability.
- Prefer type guards and restructuring over
# type: ignore.
- Treat type hints as the checked parameter contract: Pyright and mypy verify annotations but never read docstring
Args:, :param, or Returns: blocks, so a docstring that restates the signature drifts silently and no type check catches it.
- Prefer not to restate typed parameters in prose; if a project genuinely needs API docstrings, enforce them with a dedicated docstring linter, not the type checker. Prefer
pydoclint — actively maintained, runs standalone or as a pre-commit hook, and adds no new lint stack; darglint is unmaintained. Ruff's equivalent DOC rules are still preview-only, so do not pull in Ruff just for docstring checks.
Structure
- Group related modules by feature or responsibility.
- Keep tests close to the behavior they cover when the repo layout supports it.
- Extract helper modules only when the behavior is truly shared or the file has become hard to navigate.
- On modern Python baselines, do not create
__init__.py files solely to make directories importable; use implicit namespace packages unless package-level code is actually needed.
Module initialization
- Remember that
import module executes the module's entire top level, so a module-scope client = SomeClient(...) runs its work at import time and makes import order significant.
- Prefer a factory function or a lazy accessor — for example a
get_client() function, optionally memoized with functools.lru_cache — over a ready-built instance at module scope.
- Keep module-scope bindings limited to constants, type aliases, and other inert values; defer connections, configuration or environment reads, and I/O to call time.
- Treat module-scope instantiation of a stateful object as a deliberate, shared decision with a stated reason, not a default; explore a factory first. See
ref-sp-dev-coding-patterns for the portable rule.
- In tests, module-scope setup is more acceptable given small modules, but still prefer fixtures over import-time work when it could couple test order.
# avoid: runs at import time, import order now matters
client = ApiClient(os.environ["API_URL"])
# prefer: construction deferred to call time
def get_client() -> ApiClient:
return ApiClient(settings.api_url)
CLI and scripts
- If a command is part of the installed product, expose a clear
main() function and register it as an entrypoint.
- If a
uv-managed repo needs a development task for an installed dependency, prefer a Poe task that calls the dependency's console command through uv run, for example sync-shared-tool = "uv run shared-tool sync", instead of a pass-through script like python scripts/run_sync.py.
- If code is only for repo maintenance or one-off automation, keep it as a script.
- Use descriptive subcommands and flags for multi-action CLIs.
Packaging boundaries
[project.scripts] is distribution metadata, not a local convenience: it becomes entry_points.txt inside the built artifact. There is no internal-only entrypoint, so a command meant to stay in the repo belongs in the task runner instead.
- Keep repo maintenance directories out of the distributed package list. Shipping them installs test files and repo-mutating scripts into every consumer's environment.
- Never distribute a generic top-level import name such as
scripts, utils, common, or i18n. A directory with no __init__.py is a namespace package, so it merges across sys.path and silently shadows a consumer's same-named package — the consumer's own code loses, because installed paths usually sort ahead of theirs.
- Do not derive a repo root from
__file__ in a module that can be installed. Once it is copied into site-packages the path resolves to the venv rather than the checkout. Walk up from the working directory, or keep the module out of the distribution.
Testing
- Add unit tests for non-trivial logic and error cases.
- Prefer small builders, fixtures, or factory helpers over giant setup blocks.
- Keep test names specific enough that failures are easy to localize.
Example Layouts
Packaged feature with collocated tests
src/package_name/report_sync/
main.py
main_test.py
client.py
client_test.py
models.py
Repo maintenance script
scripts/
update_from_upstream.py
update_from_upstream_test.py
Give it a task-runner entry rather than a [project.scripts] one, so it keeps a first-class
invocation without becoming distribution metadata:
[tool.poe.tasks]
init-project = "python scripts/init_project.py"
Invoked as uv run poe init-project --name cool-app; a cmd-type task forwards arguments through
unchanged. The task table is read from the working tree and never lands in the built wheel, which is
what makes it internal-only in a way [project.scripts] cannot be. The payoff beyond packaging is
consistency: every documented command in the repo becomes uv run poe <task>, instead of one script
path standing out from the rest.
Validation
- Public Python code is typed clearly and reads without guesswork.
- Parameter contracts live in type hints; any prose
Args: docstrings are backed by a docstring linter, not assumed correct by the type checker.
- Modern-baseline projects do not carry legacy compatibility imports without a version-specific reason.
- Paths, errors, and data structures are explicit.
- Importing a module runs no connections or I/O; stateful clients are built by factories or lazy accessors, not at module scope.
- Product CLIs and maintenance scripts are separated intentionally, and internal commands are task-runner tasks rather than
[project.scripts] entries.
- No distributed package claims a generic top-level import name, and no installable module derives a repo root from
__file__.
- Tests cover non-trivial logic and stay readable.
References
- Read
./references/checklist.md for a quick Python review pass.
- Read
./assets/trigger-eval-queries.example.json when checking trigger quality for Python-focused prompts.
1---2name: ref-sp-py-python3description: Portable Python guidance for typed application code, scripts, CLIs, and tests. Use when: writing or refactoring Python modules, designing Python feature folders, or deciding typing and testing patterns.4license: MIT5---67# Python89## Purpose1011Provide portable Python defaults that emphasize explicit typing, simple structure, maintainable CLIs, and focused tests.1213## When to use this skill1415- Writing or refactoring Python application code.16- Designing a Python CLI or maintenance script.17- Choosing how to type shared data and interfaces.18- Deciding where tests should live and what they should cover.19- Reviewing Python code for readability and long-term maintainability.2021## Scope Boundaries2223- Use this skill for portable Python structure, typing, CLI, and testing guidance.24- Use `ref-sp-dev-coding-patterns` for language-agnostic naming, comment, and CLI ergonomics defaults.25- Use `ref-sp-dev-projects-architecture` for shared-utility thresholds and product-versus-maintenance boundaries.26- Use a repo's own repo-conventions skill (in this repo, `ref-sp-dev-repo-conventions`) when the question is about that repository's exact package names, top-level folders, or validation commands.2728## Defaults2930- Prefer modern Python with type hints throughout public and shared code.31- If the repo already targets a modern Python baseline such as 3.14+, do not add `from __future__ import annotations` or similar compatibility boilerplate just to mimic older code.32- Prefer inferred return types for local helpers when the type checker can infer them cleanly; add return annotations when the function defines an API contract or inference would hide ambiguity.33- Prefer `pathlib.Path` over raw path strings.34- Prefer dataclasses, typed dicts, or small domain objects over loose dictionaries when structure matters.35- Prefer explicit exceptions and clear error messages over silent fallbacks.36- Prefer inert module imports: defer connections, I/O, and client construction to factory functions or lazy accessors rather than running them at module scope.37- Prefer `uv` for Python dependency management, virtual environments, and runnable project commands unless the repo already mandates another Python workflow.38- In `uv`-managed repos that use Poe, prefer tasks that invoke installed console entry points through `uv run` instead of adding tiny wrapper scripts.39- Prefer the repo's standard formatter, type checker, and test task wrappers when they exist.4041## Task Framing4243| Command or action | What | Why | When | Expected outcome |44| --- | --- | --- | --- | --- |45| Organize a Python feature | Choose a feature folder, local modules, and collocated tests. | A good starting layout keeps future refactors local instead of repo-wide. | When adding a new unit of behavior. | The feature is easy to find, extend, and test. |46| Decide between product CLI and maintenance script | Choose whether a command belongs under the package or in repo maintenance paths. | Many Python repos accumulate product behavior in ad hoc scripts. | When a new command-line flow appears. | Product commands are packaged cleanly and maintenance glue stays separate. |47| Review types and tests together | Check whether the public API, data structures, and risky branches are explicit. | Python stays maintainable when type clarity and test coverage grow together. | When reviewing or refactoring non-trivial logic. | Data shapes are clear and the fragile branches are covered. |4849## Core Rules5051### Typing5253- Type function parameters clearly.54- Prefer inferred return types for private/local helpers whose implementation makes the result obvious to the checker and reader.55- Add return annotations for public APIs, shared protocol or callback contracts, abstract methods, recursive functions, overload-style dispatch, CLI entrypoints, and cases where inference would become `Any`, `object`, or an overly broad union.56- On modern Python baselines, use standard annotation syntax directly instead of future-compatibility imports for postponed annotations.57- Prefer precise container types like `list[str]` or `dict[str, int]`.58- Prefer `object` plus narrowing, focused casts, or type guards at unknown input boundaries instead of defaulting to `Any`.59- Reserve `Any` for rare interoperability gaps that cannot be expressed cleanly with narrower types.60- Use `Protocol`, `TypedDict`, dataclasses, or type aliases when they improve readability.61- Prefer type guards and restructuring over `# type: ignore`.62- Treat type hints as the checked parameter contract: Pyright and mypy verify annotations but never read docstring `Args:`, `:param`, or `Returns:` blocks, so a docstring that restates the signature drifts silently and no type check catches it.63- Prefer not to restate typed parameters in prose; if a project genuinely needs API docstrings, enforce them with a dedicated docstring linter, not the type checker. Prefer `pydoclint` — actively maintained, runs standalone or as a pre-commit hook, and adds no new lint stack; `darglint` is unmaintained. Ruff's equivalent `DOC` rules are still preview-only, so do not pull in Ruff just for docstring checks.6465### Structure6667- Group related modules by feature or responsibility.68- Keep tests close to the behavior they cover when the repo layout supports it.69- Extract helper modules only when the behavior is truly shared or the file has become hard to navigate.70- On modern Python baselines, do not create `__init__.py` files solely to make directories importable; use implicit namespace packages unless package-level code is actually needed.7172### Module initialization7374- Remember that `import module` executes the module's entire top level, so a module-scope `client = SomeClient(...)` runs its work at import time and makes import order significant.75- Prefer a factory function or a lazy accessor — for example a `get_client()` function, optionally memoized with `functools.lru_cache` — over a ready-built instance at module scope.76- Keep module-scope bindings limited to constants, type aliases, and other inert values; defer connections, configuration or environment reads, and I/O to call time.77- Treat module-scope instantiation of a stateful object as a deliberate, shared decision with a stated reason, not a default; explore a factory first. See `ref-sp-dev-coding-patterns` for the portable rule.78- In tests, module-scope setup is more acceptable given small modules, but still prefer fixtures over import-time work when it could couple test order.7980```python81# avoid: runs at import time, import order now matters82client = ApiClient(os.environ["API_URL"])8384# prefer: construction deferred to call time85def get_client() -> ApiClient:86 return ApiClient(settings.api_url)87```8889### CLI and scripts9091- If a command is part of the installed product, expose a clear `main()` function and register it as an entrypoint.92- If a `uv`-managed repo needs a development task for an installed dependency, prefer a Poe task that calls the dependency's console command through `uv run`, for example `sync-shared-tool = "uv run shared-tool sync"`, instead of a pass-through script like `python scripts/run_sync.py`.93- If code is only for repo maintenance or one-off automation, keep it as a script.94- Use descriptive subcommands and flags for multi-action CLIs.9596### Packaging boundaries9798- `[project.scripts]` is distribution metadata, not a local convenience: it becomes `entry_points.txt` inside the built artifact. There is no internal-only entrypoint, so a command meant to stay in the repo belongs in the task runner instead.99- Keep repo maintenance directories out of the distributed package list. Shipping them installs test files and repo-mutating scripts into every consumer's environment.100- Never distribute a generic top-level import name such as `scripts`, `utils`, `common`, or `i18n`. A directory with no `__init__.py` is a namespace package, so it merges across `sys.path` and silently shadows a consumer's same-named package — the consumer's own code loses, because installed paths usually sort ahead of theirs.101- Do not derive a repo root from `__file__` in a module that can be installed. Once it is copied into `site-packages` the path resolves to the venv rather than the checkout. Walk up from the working directory, or keep the module out of the distribution.102103### Testing104105- Add unit tests for non-trivial logic and error cases.106- Prefer small builders, fixtures, or factory helpers over giant setup blocks.107- Keep test names specific enough that failures are easy to localize.108109## Example Layouts110111### Packaged feature with collocated tests112113```text114src/package_name/report_sync/115 main.py116 main_test.py117 client.py118 client_test.py119 models.py120```121122### Repo maintenance script123124```text125scripts/126 update_from_upstream.py127 update_from_upstream_test.py128```129130Give it a task-runner entry rather than a `[project.scripts]` one, so it keeps a first-class131invocation without becoming distribution metadata:132133```toml134[tool.poe.tasks]135init-project = "python scripts/init_project.py"136```137138Invoked as `uv run poe init-project --name cool-app`; a `cmd`-type task forwards arguments through139unchanged. The task table is read from the working tree and never lands in the built wheel, which is140what makes it internal-only in a way `[project.scripts]` cannot be. The payoff beyond packaging is141consistency: every documented command in the repo becomes `uv run poe <task>`, instead of one script142path standing out from the rest.143144## Validation145146- Public Python code is typed clearly and reads without guesswork.147- Parameter contracts live in type hints; any prose `Args:` docstrings are backed by a docstring linter, not assumed correct by the type checker.148- Modern-baseline projects do not carry legacy compatibility imports without a version-specific reason.149- Paths, errors, and data structures are explicit.150- Importing a module runs no connections or I/O; stateful clients are built by factories or lazy accessors, not at module scope.151- Product CLIs and maintenance scripts are separated intentionally, and internal commands are task-runner tasks rather than `[project.scripts]` entries.152- No distributed package claims a generic top-level import name, and no installable module derives a repo root from `__file__`.153- Tests cover non-trivial logic and stay readable.154155## References156157- Read `./references/checklist.md` for a quick Python review pass.158- Read `./assets/trigger-eval-queries.example.json` when checking trigger quality for Python-focused prompts.