| name | python-code-style |
| description | Write Python source to this project's standards. Use when authoring or refactoring Python code and deciding on style, type hints, docstrings, comments, imports, or language idioms. |
Python Code Style
Standards for writing Python source in this project.
Style guide and docstrings
Type hints
- Type everything with the strongest type that still matches how the value is used —
specific to its semantics/interface, which for an iterated-only parameter is
Iterable[X] rather than list[X].
- Use modern type-hint syntax: lowercase built-in generics (
list, dict, tuple),
X | None over Optional[X], and collections.abc types (Iterable, Sequence,
Mapping).
- Model categorical values as a
StrEnum (or another Enum), not a bare str, so the
permitted values are explicit and type-checked.
- When a dict's keys and value types are known ahead of time, use a
TypedDict or a
dataclass rather than a plain dict, or a Pydantic model when you also need runtime
validation.
- Type-hint module-level constants with
Final, name them ALL_CAPS
(MAX_RETRIES: Final = 3), and prefix module-local (non-exported) ones with an
underscore (_MAX_RETRIES); function- and class-local values need neither Final nor
ALL_CAPS. Give magic numbers a named constant rather than inlining them.
- Don't use
typing.cast(); fix the underlying type error instead of suppressing it.
- Keep
Any to a minimum by giving values a specific type wherever one fits. For a
genuinely untypable value, prefer Any over object (which forces isinstance/casts
before use and conveys no intent).
type: ignore should almost never be used — only for third-party packages with no
typing or with dynamic typing.
- Prefer explicit typed signatures and attribute access over runtime introspection
(
getattr/hasattr/__dict__) and *args/**kwargs passthrough; both defeat static
type checking, autocomplete, and grep-based refactoring.
- Dispatch on a finite set of values with an explicit mapping or
match/case rather
than an if/elif chain.
Functions and data modelling
- Make invalid states unrepresentable: model data so bad combinations can't be
constructed — an
Enum instead of several booleans, a sub-model instead of loosely
related flat fields, one canonical representation of "empty" — and minimise
optional/None-able fields. Prefer it to runtime validation that patches bad input.
- Don't mutate objects passed in as arguments; copy and return a new value (recreate
Pydantic models). In-place mutation is a surprising side effect that hampers reasoning
and testing.
- Make every parameter keyword-only with a leading bare
* when any of these holds:
three or more parameters, two or more of the same type, or any boolean parameter
(prevents argument-order and boolean-trap bugs).
- Group related values that travel together into a named dataclass/model rather than a
positional tuple, and defer extracting a shared helper until the duplication is real.
Comments and docstrings
- Keep comments to an absolute minimum. Achieve readability through meaningful class,
function, and variable names instead — and prefer extracting a well-named intermediate
variable over a comment that explains a complex expression.
- Suffix variables and function arguments with their unit of measure where one applies
(e.g.
timeout_ms, payload_size_bytes, duration_seconds), so the name carries the
unit.
- Name for meaning rather than implementation, write boolean predicates positively to
avoid double negation, don't use generic module names like
utils.py, and keep
acronyms fully upper-case (HTTPClient, not HttpClient).
- Don't write module- or class-level docstrings. Keep function docstrings only, and
short; put a CLI's usage narrative in its command/argument help (e.g. Typer), not a
module docstring.
- Public functions should have docstrings. Parameters only need to be documented when
the name and type hint don't convey their full semantics.
- Always document non-obvious side effects, the exceptions a function raises, and
whether numeric range bounds are inclusive or exclusive, even when the parameters
themselves are self-evident.
- Private functions (single-underscore prefix, per PEP 8) used within a module don't need
docstrings, unless their names and type hints aren't sufficient to convey their
semantics.
- Comments should only be used to explain:
- unavoidable code smells arising from third-party package use,
- the reason for temporary dependency version pinning (e.g. linking an unresolved
GitHub issue), or
- opaque code, non-obvious trade-offs, or workarounds.
- Link every suppression (
noqa, type: ignore, a coverage pragma) to a tracking
issue or a one-line justification.
Imports
- Keep all imports at the top of the module unless an import must be local to avoid a
circular import.
- Use absolute imports only — no relative imports.
- Don't import private (underscore-prefixed) names across modules; if another module
needs it, make it public and place it in the module its domain belongs to.
- Keep
__init__.py files empty: no re-exports; import each symbol from the module that
defines it. (Exception: deliberately curated public-API packages populate
__init__.py to simplify import paths.)
- Don't add
from __future__ import annotations; the project's Python version defers
annotation evaluation already.
Language idioms
- Use the most modern Python idioms and syntax allowed by the project's minimum
supported Python version.
- Prefer immutable containers (
tuple, frozenset) for fixed or read-only data,
including module-level constants; use mutable containers only when the data changes,
and default_factory for any mutable dataclass/Pydantic default.
- Use
pathlib.Path (not os/os.path) for filesystem work and as path-argument
types, and reach for stdlib helpers (Counter, itertools, str.removeprefix, //)
over hand-rolled equivalents.
- Build collections with comprehensions or generator expressions rather than
loop-and-
append. Pass a generator (not a materialised list) to one-shot consumers
like str.join, sum, or pd.concat.
- Use
contextlib.suppress(...) instead of an empty try/except: pass; use float
literals in float arithmetic and never compare floats with == (use math.isclose or
an explicit tolerance).
- Use a plain string literal, not an f-string, when there's nothing to interpolate.
- Keep non-trivial SQL in
.sql files loaded at runtime, not embedded in Python string
literals, so SQL tooling can lint and format it.
- Compare versions with
packaging.version.Version, not string or tuple comparison
(which sorts e.g. 0.10 before 0.9).
Testing
Test authoring and layout conventions live in the python-pytest skill.