npx skills add ...
npx skills add nathan-gage/python-skills --skill python-best-practices
Python software engineering guidelines from real PR review patterns. This skill should be used when writing, reviewing, or refactoring Python code — especially dataclasses, service interfaces, error handling, and type annotations. Triggers on tasks involving Python modules, API design, data modeling, type safety, exception handling, or refactoring for maintainability. For asyncio and concurrency guidance, see the python-async-best-practices skill.
npx skills add nathan-gage/python-skills --skill python-best-practices
Guidelines for writing and reviewing Python. 75 rules across 8 categories, prioritized by impact.
A rule match is a signal, not a verdict. Most rules are design preferences for new code, not bugs to fix across the repo — check the rule's impact level before flagging in review or refactoring stable code.
Quick-reference lines are triggers, not licenses: before applying a rule as a review finding or a transformation, open the rule file and check its counter-signal — the marker-opened paragraph (**When ...** / **Scope:** / **Preserve ...**) saying when NOT to apply it.
Avoid applying these rules as a blanket sweep across stable code — the churn rarely pays off.
CRITICAL — prevents a real bug class (data corruption, swallowed cancellations, insecure defaults). Fix when found.HIGH — meaningful correctness or maintainability win. Worth fixing in most contexts.MEDIUM — good practice; clarity or drift prevention. Apply to new code; don't churn stable code.LOW-MEDIUM / LOW — style or micro-optimizations. Apply opportunistically.Rules assume Python 3.11+. Rules depending on higher versions call it out inline:
warnings.deprecated() — 3.13+zoneinfo — 3.9+isinstance() — 3.10+assert_never — 3.11+ (backport via typing_extensions)type statement and generic syntax — 3.12+ (noted inline in types-modern-syntax)Rules tagged applicability:pydantic are Pydantic-specific.
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Data Modeling | HIGH | data- |
| 2 | Error Handling | MEDIUM-HIGH | error- |
| 3 | Type Safety | MEDIUM-HIGH | types- |
| 4 | API Design | MEDIUM | api- |
| 5 | Code Simplification | LOW-MEDIUM | simplify- |
| 6 | Performance | LOW-MEDIUM | perf- |
| 7 | Naming | LOW-MEDIUM | naming- |
| 8 | Imports & Structure | LOW | imports- |
Section impact is a typical-case label; individual rules range one level above or below — check the rule file.
data-)data-mutable-defaults — Never def f(items=[]); use None + body construction or default_factorydata-derive-dont-store — Compute booleans from state; don't cache flags that mirror each otherdata-mutation-contract — One unambiguous contract per function: mutate (new-info returns fine) or return new — never the mutated object as if freshdata-aware-datetimes — Timezone-aware datetime.now(timezone.utc); utcnow() is deprecateddata-discriminated-unions — Tag variants instead of optional-field bagsdata-explicit-variants — Concrete classes per mode beat is_thread / is_edit flagsdata-phased-composition — Group co-present optionals into one nested optionaldata-encapsulate-mutable-state — Trap mutable state in the narrowest clear scopedata-sentinel-when-none-is-valid — Private sentinel when None is a meaningful valuedata-newtype-for-ids — NewType('UserId', str) so IDs aren't interchangeabledata-delete-dead-variants — Remove union arms that aren't constructeddata-reject-bool-as-int — bool subclasses int; reject it explicitly before numeric checkserror-)error-specific-exceptions — Catch specific types; never bare except: or except BaseException: (breaks Ctrl-C and async cancellation); except Exception: is cancellation-safe on 3.8+error-context-managers — with / async with for files, locks, sessionserror-assert-debug-only — assert vanishes under -O; not for runtime contractserror-validate-at-boundaries — Fail fast at system edges before expensive workerror-trust-validated-state — Trust immutable, locally-constructed stateerror-consolidate-try-except — Merge blocks with the same catch and handlingerror-assert-never-exhaustiveness — typing.assert_never for exhaustivenesserror-raise-from-for-chains — raise NewErr(...) from original to preserve causalityerror-inherit-base-exceptions — New exceptions inherit existing bases for compatibilityerror-log-exception-context — logger.exception(...) inside except; keep the traceback in the logerror-repr-in-messages — f"tool {name!r}" for identifiers in error texterror-match-types-not-messages — Classify by exception type and status code, never message substringstypes-)types-fix-errors-not-ignore — Fix type errors; # type: ignore is a last resorttypes-avoid-any — Protocols, TypeVars, unions over Anytypes-typeddict-over-dict-any — TypedDict / dataclass when structure is knowntypes-literal-for-fixed-sets — Literal["a", "b"] for fixed stringstypes-fix-types-not-cast — Fix the definition; cast() only when runtime genuinely narrowstypes-isinstance-for-narrowing — isinstance() over hasattr / type(x).__name__types-narrow-to-runtime-reality — Annotations match what control flow actually allowstypes-trust-the-checker — Drop runtime checks the types already enforcetypes-remove-redundant-optional — Drop | None when values are guaranteed presenttypes-type-checking-imports — if TYPE_CHECKING: for optional or heavy importstypes-modern-syntax — X | None, list[str]; not Optional / Union / typing.Listtypes-sequence-over-list-params — Sequence / Mapping for read-only params; list is invariantapi-)api-required-before-optional — Required fields before optional (Python enforces this)api-keyword-only-params — * marker for optional/config paramsapi-no-boolean-flag-params — Literal / Enum over True, False soupapi-immutable-transforms — Return new collections; don't mutate inputsapi-model-cohesion — Flat models; no duplicate or single-key-wrapped fieldsapi-underscore-for-private — _prefix for internals; exclude from __all__api-deprecated-aliases — warnings.deprecated() (3.13+) for renamed APIsapi-no-private-access — Don't reach into _prefixed names from outside the moduleapi-instance-vs-module-fn — Pick the namespace that matches ownershipsimplify-)simplify-early-return — Return early; don't nest the happy pathsimplify-extract-after-duplication — Second copy is the decision point; third is the safe defaultsimplify-cached-property — @cached_property on immutable instances; not thread-safesimplify-comprehensions — Comprehensions over for + .append()simplify-any-all-builtins — any() / all() over manual flag + breaksimplify-fallback-or — x or default when falsy values aren't semanticsimplify-flatten-nested-if — if cond1 and cond2: when no intervening codesimplify-inline-single-use-vars — Drop intermediates used oncesimplify-remove-dead-code — Delete commented-out code; git preserves historyperf-)perf-set-for-membership — set for repeated in checksperf-dict-index-over-nested-loops — Build a dict for lookupsperf-lru-cache-pure-fns — functools.lru_cache / functools.cache on pure functionsperf-generator-over-list — Stream with generators when memory or latency mattersperf-combine-iterations — Fuse filter + map into one passperf-compile-regex-module-level — Compile static regex at module scope; matters in tight loopsperf-type-adapter-constant — Module-scope TypeAdapter (applicability: pydantic)perf-isinstance-tuple-syntax — Tuple form is marginally faster; profiled hot paths onlynaming-)naming-rename-on-behavior-change — Rename when behavior changes; stale names misleadnaming-consistent-terminology — Same concept, same word across code/docs/errorsnaming-specific-over-generic — toolset_id; not bare idnaming-drop-redundant-prefixes — ToolConfig.description; not ToolConfig.tool_descriptionnaming-upper-case-constants — MAX_RETRIES; _ prefix for internalnaming-no-type-suffixes — No _dict / _list suffixes; types annotate typesimports-)imports-no-side-effects — Modules must be cheap to import — no network/model/env reads at importimports-top-of-file — Imports at the top; documented exceptions for circular / optional / deferredimports-optional-dependencies — try / except ImportError with install hintsimports-scope-helpers-to-usage — Define helpers near where they're usedimports-remove-unused — Delete unused importsimports-no-duplicates — One import per nameimports-lightweight-init — Parent __init__.py runs on every submodule import; keep heavy/optional deps outRead individual rule files for detail:
Each rule has:
For the full compiled guide with all rules expanded: AGENTS.md.