Python Best Practices
Guidelines for writing and reviewing Python. 70 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.
When to Apply
- Writing new Python modules, functions, classes, or data models
- Reviewing code for correctness or type safety
- Refactoring patterns in code that's being edited anyway
Avoid applying these rules as a blanket sweep across stable code — the churn rarely pays off.
Impact Levels
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.
Python Version Baseline
Rules assume Python 3.11+. Rules depending on higher versions call it out inline:
warnings.deprecated()— 3.13+zoneinfo— 3.9+- Union types in
isinstance()— 3.10+ assert_never— 3.11+ (backport viatyping_extensions)
Rules tagged applicability:pydantic are Pydantic-specific.
Rule Categories by Priority
| 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.
Quick Reference
Data Modeling (data-)
data-mutable-defaults— Neverdef f(items=[]); useNone+ body construction ordefault_factorydata-derive-dont-store— Compute booleans from state; don't cache flags that mirror each otherdata-mutation-contract— Mutate OR return; not bothdata-aware-datetimes— Timezone-awaredatetime.now(timezone.utc);utcnow()is deprecateddata-discriminated-unions— Tag variants instead of optional-field bagsdata-explicit-variants— Concrete classes per mode beatis_thread/is_editflagsdata-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 whenNoneis a meaningful valuedata-newtype-for-ids—NewType('UserId', str)so IDs aren't interchangeabledata-delete-dead-variants— Remove union arms that aren't constructed
Error Handling (error-)
error-specific-exceptions— Catch specific types; never bareexcept:orexcept BaseException:(breaks Ctrl-C and async cancellation);except Exception:is cancellation-safe on 3.8+error-context-managers—with/async withfor files, locks, sessionserror-assert-debug-only—assertvanishes 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_neverfor exhaustivenesserror-raise-from-for-chains—raise NewErr(...) from originalto preserve causalityerror-inherit-base-exceptions— New exceptions inherit existing bases for compatibilityerror-log-exception-context—logger.exception(...)insideexcept; keep the traceback in the logerror-repr-in-messages—f"tool {name!r}"for identifiers in error text
Type Safety (types-)
types-fix-errors-not-ignore— Fix type errors;# type: ignoreis a last resorttypes-avoid-any— Protocols, TypeVars, unions overAnytypes-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()overhasattr/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| Nonewhen values are guaranteed presenttypes-type-checking-imports—if TYPE_CHECKING:for optional or heavy imports
API Design (api-)
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/EnumoverTrue, Falsesoupapi-immutable-transforms— Return new collections; don't mutate inputsapi-model-cohesion— Flat models; no duplicate or single-key-wrapped fieldsapi-underscore-for-private—_prefixfor internals; exclude from__all__api-deprecated-aliases—warnings.deprecated()(3.13+) for renamed APIsapi-no-private-access— Don't reach into_prefixednames from outside the moduleapi-instance-vs-module-fn— Pick the namespace that matches ownership
Code Simplification (simplify-)
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_propertyon immutable instances; not thread-safesimplify-comprehensions— Comprehensions overfor+.append()simplify-any-all-builtins—any()/all()over manual flag +breaksimplify-fallback-or—x or defaultwhen 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 history
Performance (perf-)
perf-set-for-membership—setfor repeatedinchecksperf-dict-index-over-nested-loops— Build adictfor lookupsperf-lru-cache-pure-fns—functools.lru_cache/functools.cacheon pure functionsperf-generator-over-list— Stream with generators when memory or latency mattersperf-combine-iterations— Fusefilter+mapinto one passperf-compile-regex-module-level— Compile static regex at module scope; matters in tight loopsperf-type-adapter-constant— Module-scopeTypeAdapter(applicability: pydantic)perf-isinstance-tuple-syntax— Tuple form is marginally faster; profiled hot paths only
Naming (naming-)
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 bareidnaming-drop-redundant-prefixes—ToolConfig.description; notToolConfig.tool_descriptionnaming-upper-case-constants—MAX_RETRIES;_prefix for internalnaming-no-type-suffixes— No_dict/_listsuffixes; types annotate types
Imports & Structure (imports-)
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 ImportErrorwith install hintsimports-scope-helpers-to-usage— Define helpers near where they're usedimports-remove-unused— Delete unused importsimports-no-duplicates— One import per name
How to Use
Read individual rule files for detail:
rules/data-mutable-defaults.md
rules/error-specific-exceptions.md
Each rule has:
- Impact level in frontmatter
- Brief explanation
- Incorrect example
- Correct example
- Optional note on edge cases
For the full compiled guide with all rules expanded: AGENTS.md.

