How to Use a Python Rules List
The pages in this section - a 50-item master list, plus 30-item banks for async code, security, and data/ML work, plus a 40-item bank for API design - can look, at a glance, like the same content chopped into different-sized lists. They aren't. Each one exists because a specific domain of Python work has failure modes the general list can't cover without becoming either too long to use or too vague to enforce, and the split itself is a design decision worth understanding before you use any individual list.
This page explains that design: why rules are grouped the way they are, what separates a rule you can put in a linter from one you can only put in a code review checklist, and how a rules list is meant to function day to day rather than sit unread in a wiki. It doesn't restate the rules themselves - 50 Python Rules Every Specialist Should Follow and the domain-specific banks already do that.
Summary
- A rules list is a codified set of team defaults, split by domain because different areas of Python work (async, security, data/ML, API design) fail in different, specific ways.
- Insight: Used correctly, a rules list removes repetitive debate from code review; used as dogma, it produces either alert fatigue or rules applied where they don't fit the context.
- Key Concepts: enforceable rule, judgment-call rule, domain-specific bank, rule numbering as navigation, documented exception.
- When to Use This Model: Deciding which list applies to a piece of code, setting up linter/CI rules from a list, or handling a disagreement about whether a rule applies in a specific case.
- Limitations/Trade-offs: A rules list captures what a team already knows to watch for - it can't anticipate every new failure mode, and treating it as complete is itself a risk.
- Related Topics: static analysis configuration, code review standards, onboarding documentation, architecture decision records.
Foundations
A rule, in the sense this section uses the word, is a codified default: a decision a team has already made once, in general, so it doesn't have to be re-argued in every pull request.
That's different from a law - a rule can be overridden when the specific context calls for it, as long as the override is deliberate and visible, not silently ignored.
The clearest split within any list here is between rules that are mechanically enforceable and rules that require judgment.
An enforceable rule has a tool that can check it automatically - ruff catching an unused import, mypy catching a missing type hint, a pre-commit hook catching a hardcoded secret pattern - so a human reviewer never needs to manually verify it.
A judgment-call rule can't be checked by a tool at all - "specific exceptions over bare except: pass" requires understanding what the code is actually trying to do, and "dependency injection over globals" requires understanding the design, not just scanning syntax.
Both kinds of rules belong on a useful list, but conflating them is a common mistake: expecting a linter to enforce a judgment call leads to disappointment, and expecting a human reviewer to reliably catch what a linter should have caught leads to inconsistency.
Mechanics & Interactions
This section's structure - a general master list plus separate domain banks - exists because domain-specific failure modes don't fit cleanly into a single flat list without either bloating it past usefulness or forcing unrelated rules to share the same section.
50 Python Rules Every Specialist Should Follow covers what applies broadly: style, types, structure, testing, and baseline security, organized into numbered groups (1-10 style, 11-20 types, and so on).
That numbering is a navigation aid, not a priority order - rule 45 (parameterized SQL) is not less important than rule 5 (f-strings) just because it comes later in the list; the groups exist so a reader can jump to the relevant section, not so the rules can be ranked by number.
30 Async Rules, 30 Security Rules for Python, and 30 Data/ML Code Rules exist as separate banks because each domain has traps invisible from a general Python perspective: asyncio has specific footguns around blocking calls inside coroutines and forgotten awaits that don't exist in synchronous code at all, security has an entire OWASP-shaped threat model that a general style guide was never designed to cover, and data/ML pipelines have reproducibility and data-leakage failure modes that look nothing like a typical web service bug.
40 API Design Rules (FastAPI/Django/Flask) is domain-specific in the same way, scoped to the framework layer where request/response contracts, versioning, and error-shape consistency live - concerns the general list touches only in passing.
# A rule made mechanically enforceable, and one that can't be:
# ruff rule B008 catches this automatically:
def handler(items=[]): # mutable default - flagged by static analysis
...
# but no tool can check this one:
# "separate domain logic from I/O so it's testable without mocks"
# - that's an architecture judgment a reviewer has to make by reading the code.The takeaway from that split is practical: when adopting a list, map every enforceable rule to an actual ruff select-code, mypy setting, or pre-commit hook, and leave judgment-call rules to review checklists and onboarding conversations - trying to force the second kind into automation produces either false confidence or rules nobody can articulate why they exist.
Advanced Considerations & Applications
A rules list only stays useful if a team treats deviations as something to document, not something to hide.
Python Architecture Decisions is the natural home for that documentation - a genuine exception to a rule ("we used a mutable global cache here because X") belongs in a written decision record, not a silent comment or an unexplained diff, because the next engineer reading that code needs to know the exception was deliberate.
This matters more as a list grows: a 50-item list adopted all at once, with every ruff rule enabled on day one, tends to produce alert fatigue - reviewers start ignoring warnings wholesale because too many fired at once, which defeats the purpose of automating any of them. Adopting incrementally, group by group, keeps each new automated check meaningful instead of noise.
The other axis worth being deliberate about is scope: a one-off internal script reasonably relaxes structural rules (module organization, dependency injection) that a shared library cannot, while security rules (parameterized queries, secret handling) and core style rules apply everywhere regardless of how throwaway the code seems - "it's just a script" is not a defense against a SQL injection.
| List type | Enforcement | Typical audience | Risk if misused |
|---|---|---|---|
| Master list (general) | Mixed - some ruff/mypy, some review | All engineers, onboarding | Applied too rigidly to code where scope doesn't fit (scripts, prototypes) |
| Domain bank (async/security/data-ML) | Mostly review + domain-specific tooling (bandit, pip-audit) | Engineers working in that domain | Treated as optional because it's "specialized," missed on cross-domain code |
| Style/PEP 8 reference | Almost entirely enforceable via ruff format | Every contributor, via CI gate | Manual style debates in review despite a tool already deciding it |
| Problem/solution reference | Not a rule list at all - idiom lookup | Engineers unsure of the idiomatic approach | Confused with a rules list; it documents "how," not "must" |
| Architecture decisions | Not enforceable - a record of judgment calls made | Reviewers evaluating exceptions to other rules | Treated as optional documentation instead of the source of truth for "why we deviated" |
Reading a rules list well means recognizing which row a given page falls into before applying it - the PEP 8 & Style Reference and Problem / Solution Reference aren't prescriptive rule banks in the same sense as the numbered lists; they're reference material the numbered lists assume you can look up when a rule references a convention or an idiom by name.
Common Misconceptions
- "A rules list is a complete specification of correct code." It's a codified set of defaults reflecting failure modes the team already knows about; new failure modes - a new library's footgun, a new attack pattern - won't appear on a list written before anyone hit them.
- "Every rule on the list should be enforced by CI." Only the mechanically checkable ones can be - forcing a judgment-call rule ("separate domain from I/O") into an automated gate either produces false positives or requires a linter to understand intent, which none currently do.
- "The numbering (1-50, 1-30) reflects priority." It reflects grouping for navigation - rule 45 isn't less important than rule 5 because a later group happens to come later in the document.
- "A domain-specific bank is optional if you already know the general rules." Domain-specific failure modes (asyncio's blocking-call traps, ML's data leakage) are largely invisible from general Python knowledge alone - the general list wasn't designed to catch them and doesn't try to.
- "Deviating from a rule means the rule was wrong." A documented, deliberate exception for a specific context is a normal and healthy use of a rules list; the failure mode is an undocumented exception a future reader has no way to distinguish from a mistake.
FAQs
What's the difference between a rule and a law in this context?
A rule is a codified default the team agreed on so it doesn't need re-arguing every PR, but it can be deliberately overridden with a documented reason; a law implies no legitimate exception, which isn't how any list in this section is meant to be read.
How do I know if a rule can be automated?
Ask whether a tool could check it purely from syntax or types without understanding the code's intent - "no bare except:" is checkable; "separate domain logic from I/O" requires a human to judge the actual design, so it stays a review-time rule.
Why does this section have a 50-item list AND separate 30-item lists for async, security, and data/ML?
Because each of those domains has failure modes invisible from general Python knowledge - asyncio-specific footguns, a security threat model, ML-specific reproducibility and leakage risks - that would either bloat a general list past usefulness or get diluted if merged into it.
Does the numbering inside a list (like 1-10, 11-20) mean anything beyond navigation?
It groups related rules by domain (style, types, structure, testing, security) so a reader can jump to the relevant section quickly; it isn't a ranking of importance, and later groups aren't lower priority than earlier ones.
Should a one-off internal script follow every rule on the master list?
Structural rules (module layout, dependency injection, src layout) are reasonably relaxed for a throwaway script, but security and core correctness rules - parameterized queries, secret handling, input validation - still apply regardless of how small the script is.
What should happen when a team decides to break a rule on purpose?
The exception should be documented, typically in an architecture decision record, explaining the specific reason - an undocumented exception is indistinguishable from an oversight to the next engineer who reads that code.
Why not just enable every ruff/mypy rule on day one for maximum coverage?
Enabling everything at once tends to produce more warnings than a team can meaningfully act on, and reviewers start ignoring the noise wholesale - adopting rule groups incrementally keeps each new automated check something people actually respond to.
Is the PEP 8 & Style Reference page a rules list too?
It's reference material the numbered rules point to - it documents the conventions in detail rather than asserting new "must-do" rules of its own, which is a different role from the numbered banks.
How does the Problem/Solution Reference page relate to the rules lists?
It's an idiom lookup - "how do I do X the Pythonic way" - not a prescriptive rules list; it answers a "how" question the rules lists don't spend space on, since a rule like "use pathlib.Path" assumes you already know roughly how to use it.
Who is the master list meant for versus a domain-specific bank?
The master list targets every engineer regardless of what they're building, typically used in onboarding and general code review; a domain bank targets engineers actively working in that domain - async services, security-sensitive code, or ML pipelines - where the general list's coverage runs out.
What's the actual risk of treating a rules list as complete and final?
It creates false confidence that following the list guarantees correctness, when the list only reflects failure modes the team already knew about at the time it was written - new tools, new libraries, and new attack patterns won't be on it until someone updates it.
How often should a rules list actually be revisited?
Whenever the language, framework, or tooling has a major release, or after a real incident reveals a failure mode the list didn't cover - a static list quietly goes stale the same way any other documentation does if nothing prompts a review.
Related
- 50 Python Rules Every Specialist Should Follow - the general master list this page sits above
- 30 Async Rules - domain-specific asyncio failure modes
- 30 Security Rules for Python - domain-specific threat model
- 40 API Design Rules (FastAPI/Django/Flask) - framework-layer contract rules
- Python Architecture Decisions - where documented exceptions to rules belong
- PEP 8 & Style Reference - the reference material the style rules point to
Stack versions: This page is conceptual and not tied to a specific stack version.