python-design
Skill2 starsChanged 4 months agoMITView on GitHub
You MUST consult this skill when writing, reviewing, or generating Python code. Also trigger on Python-specific design choices (Protocol vs ABC, TypedDict vs dataclass, Pydantic boundaries), code style anti-patterns (broad exceptions, sentinel defaults, redundant docstrings, unnecessary future imports), or advanced patterns (functools, generator pipelines, typing). NOT for debugging Python errors, framework-specific setup (Django, Flask, FastAPI config), package management, or writing tests (see test-design).
1 other project uses a version of this.
Adopted by
Most recently taken up by
- mindfold-ai/Trellistheir own version, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- mindfold-ai/Trellis★ 15ksame name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository67 other files it gives its agents.
- AGENTS.mdAGENTS.md
- agent-architectureSkill
- agent-evaluationSkill
- agent-memorySkill
- agent-post-trainingSkill
- agent-self-evolutionSkill
- agent-tool-designSkill
- api-designSkill
- application-architectureSkill
- brainstormSkill
- code-reviewerSkill
- coding-agent-designSkill
- concurrency-designSkill
- context-engineeringSkill
- dockerSkill
- game-audioSkill
- game-designSkill
- game-narrativeSkill
- game-patternsSkill
- game-performanceSkill
- game-renderingSkill
- game-visualsSkill
- git-commitSkill
- git-prSkill
- godot-shaderSkill
- godotSkill
- gpu-rendering-architectureSkill
- grafanaSkill
- helmSkill
- homelab-monitoringSkill
- k3sSkill
- k8s-networkingSkill
- k8s-operationsSkill
- k8s-storageSkill
- k8s-workloadsSkill
- knowledge-baseSkill
- level-designSkill
- llm-inference-servingSkill
- loggingSkill
- logqlSkill
- love2d-fennelSkill
- love2dSkill
- ml-platform-architectureSkill
- ml-post-trainingSkill
- multi-agent-collaborationSkill
- nix-dendriticSkill
- nix-packagingSkill
- nixSkill
- odin-designSkill
- odin-gamedevSkill
- orchestratorSkill
- physical-ai-systemsSkill
- plannerSkill
- prd-to-storiesSkill
- prd-writingSkill
- promqlSkill
- pr-reviewerSkill
- rag-designSkill
- ray-data-pipelinesSkill
- ray-kubernetes-infrastructureSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 8 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Use these tables as a first pass, then read the relevant reference for implementation details and worked examples.
- Very specific: line 35
TypedDict vs frozen dataclass: If the data has a domain name (OrderSummary, UserStats) — use a frozen dataclass.
- Very specific: line 95
Use __post_init__ validation or a class-method factory to enforce constraints on construction.
- Very specific: line 130
When None spreads through domain code, the root cause is usually raw external data entering the domain core without refinement (see above).
- Distinctive: line 202
If you can't write a one-paragraph module docstring that accurately describes what the module does, the module has too many …
- Distinctive: line 234
Renaming test: if you had to rename a module so a new engineer finds it on their first try, what would you call it?
- Very specific: line 243
Only add it for genuine circular type references via TYPE_CHECKING.
- Very specific: line 249
Never bare except: or except BaseException — these catch KeyboardInterrupt and SystemExit.
- Very specific: line 280
Use .get("key") without a default.
- Very specific: line 320
Omit sections that restate what the signature already says: If Args names and types tell the whole story, or Returns type is …
- Very specific: line 348
Use with_suffix, with_stem, with_name instead of string concatenation or f-string path assembly.
- Distinctive: line 364
All imports belong at the top of the file — never inside functions.
- Distinctive: line 383
No docstrings that restate the signature.
- Distinctive: line 384
No comments that restate the code.
- Distinctive: line 385
No single-use type aliases.
- Distinctive: line 386
No unnecessary helper functions that are only called once and don't simplify.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Python Design Patterns
Python-specific patterns for expressing clean software design. For the
underlying design philosophy (why small interfaces, why composition, why
immutability), see the software-design skill.
Choosing the Right Abstraction
Use these tables as a first pass, then read the relevant reference for implementation details and worked examples.
Data containers — pick based on what the data is, not just its shape:
| Situation | Reach for | Why |
| External input needing validation (API, config, files) | Pydantic BaseModel | Validates + coerces at the boundary |
| JSON/API input that stays dict-compatible (already trusted) | TypedDict | Zero overhead, serializable, type-safe |
| Domain object with a meaningful name (Order, UserSummary) | @dataclass(frozen=True) | Immutable, hashable, identity beyond "bag of keys" |
| Lightweight immutable record, supports unpacking | NamedTuple | Lighter than dataclass, tuple semantics |
| Fixed set of choices (statuses, modes, event types) | Enum / StrEnum | Prevents typos, exhaustive matching |
| Collection that shouldn't mutate after creation | tuple / frozenset | Signals intent, prevents accidental mutation |
TypedDict vs frozen dataclass: If the data has a domain name
(OrderSummary, UserStats) — use a frozen dataclass. If the data is
genuinely dict-shaped and must stay a plain dict for serialization or
library interop — use TypedDict. The access pattern tells you:
data["user_id"] → TypedDict; summary.total_revenue → dataclass.
Literal vs Enum: For fields on Pydantic models with a small, stable
set of values, Literal["idle", "busy", "dead"] gives you validation and
IDE autocomplete without a separate class. Reach for Enum/StrEnum when
the set is reused across multiple models or needs methods/behavior. Note
that Literal values are not iterable and have no runtime members — if you
ever need to enumerate valid values or use them in a match guard, use
StrEnum instead. This complements the Enum/StrEnum row in the table
above — use Literal only when the field is a local discriminator, not a
shared domain concept.
Interface contracts:
| Need | Reach for | Why |
| Caller-facing contract | Protocol | Structural typing, no inheritance coupling |
| Shared implementation for authors | ABC | Callers type against Protocol, not the ABC |
| Shared behavior across unrelated classes | Mixin | Implementation without contract |
Generics restraint: TypeVar and generics shine in container and collection abstractions — type-safe wrappers, reusable data structures, utility functions like first(items: Sequence[T]) -> T. The trap is over-genericizing business logic that only ever has one concrete type: if UserRepository will never be anything other than a UserRepository, making it Repository[T] adds abstraction without value. Even when multiple concrete types exist, if every subclass adds domain-specific methods anyway, the generic base captures only the trivial skeleton — evaluate whether that skeleton justifies the abstraction overhead. A caller-facing Protocol often provides more value than a generic concrete base. Reach for generics when the type relationship is genuinely reusable across multiple concrete types; otherwise, name the type directly. See references/advanced-patterns.md for TypeVar syntax and examples.
Collection semantics: Match the collection to the operation — set for
membership tests, list for ordered/indexed access, dict for keyed
lookup. Using a dict for membership checks or a list for uniqueness
signals unreviewed code.
Key Patterns
Validation Boundaries
Pydantic BaseModel at the edges, frozen dataclasses inside. External data
(APIs, config, CLI) gets validated by Pydantic, then converted to immutable
domain types. This prevents Pydantic's validation overhead and mutability
from spreading through internal logic.
class CreateOrderRequest(BaseModel): # boundary — validates external input
product_id: str
quantity: int
price: Decimal
@dataclass(frozen=True)
class Order: # internal — immutable, no validation overhead
product_id: str
quantity: int
price: Decimal
def create_order(request: CreateOrderRequest) -> Order:
return Order(**request.model_dump())Refine fields into proven types. The domain object should carry proof
that its fields satisfy domain invariants — not just copy the raw values.
Use __post_init__ validation or a class-method factory to enforce
constraints on construction; downstream code then needs no re-checks.
@dataclass(frozen=True)
class Order:
product_id: str # ponytail: NewType('ProductId', str) if ID format matters
quantity: int
price: Decimal
def __post_init__(self) -> None:
if self.quantity <= 0:
raise ValueError(f"quantity must be positive, got {self.quantity}")
if self.price <= 0:
raise ValueError(f"price must be positive, got {self.price}")
def create_order(request: CreateOrderRequest) -> Order:
# Raises on invalid data — caller holds an Order known to be valid.
return Order(
product_id=request.product_id,
quantity=request.quantity,
price=request.price,
)Enforcement is runtime: the smart constructor raises on bad data; type
checkers (pyright/mypy) narrow types after isinstance/assert but cannot
prove field-level numeric constraints from the type alone. Do not claim
compile-time proofs. The value is that all callers are protected by one
construction-time guard, not per-caller re-checks. See
software-design/references/type-driven-design.md for the language-agnostic
principle.
Avoiding None in Domain Logic
When None spreads through domain code, the root cause is usually raw
external data entering the domain core without refinement (see above). When
the refined-type approach doesn't cover a case:
- Better defaults — use
[],False, or0instead ofNonewhen empty is valid. - Raise instead of returning
None— domain exception makes the return type non-optional. - Model states as distinct classes —
OfflineDrone,ConnectedDrone,ReadyDrone; each function accepts only the state it needs. - Null Object —
NullDiagnosticsimplementing the sameProtocoleliminatesif x is not None:guards. - Sentinel —
MISSING = object()distinguishes "caller passedNone" from "caller omitted the argument." - Result types (
Route | RouteFailed) — ArjanCodes argues prefer exceptions over Result types in Python; thereturnspackage exists but is not recommended for typical Python code.
Protocols Over ABCs for Callers
Define caller-facing interfaces as Protocol. Any object with matching
methods satisfies the contract — no inheritance required. Use ABCs only when
implementors need shared logic; callers always type against the Protocol.
Composition via Functions, Not Class Hierarchies
Python's composition strength comes from generators, itertools,
functools, and first-class functions. Prefer standalone functions over
methods for operations on objects (serialization, formatting,
orchestration). Generator pipelines compose streaming transforms without
materializing intermediate collections.
Shallow Error Hierarchies
One base exception per domain, one level of specific errors beneath it.
Callers catch the base or the specific — nothing deeper. Use context
managers (contextlib.contextmanager) for resource cleanup.
Module Hygiene
Define __all__ in every module with a public API — place it after imports.
Structure modules so readers encounter the public interface first. Run
pyright or mypy in CI to enforce Protocol conformance and catch type
mismatches.
__all__ = ["process_orders", "Order", "OrderSummary", "OrderError"]Split a module when it exceeds ~300 lines or has 3+ distinct
responsibilities — whichever comes first. Prefer flat structure
(src/formatters.py) over deep nesting (src/utils/helpers/formatters/date.py).
__init__.py as Design Document
A module's __init__.py with explicit __all__ is a design document — it
defines the public interface and forces deliberate decisions about what to
expose.
# app/payments/__init__.py
"""Payment processing domain.
Public interface:
- charge(amount, payment_method) -> Receipt
- refund(receipt) -> Refund
- list_charges(account_id) -> list[Receipt]
"""
from .charge import charge
from .refund import refund
from .queries import list_charges
__all__ = ["charge", "refund", "list_charges"]
# Everything else is implementation detail
# _processor, _validation, _webhook_handler are NOT exportedIf you can't write a one-paragraph module docstring that accurately describes what the module does, the module has too many responsibilities.
Naming as Information Scent
Strong names communicate domain concept, not mechanism. Weak names could describe code in five different modules — they're placeholders, not names.
Strong scent (domain concept):
payment_processor,invoice_generator,user_authenticatorscheduled_for,expires_at,cancelled_reasonSeatReservation,FlightManifest,BoardingPass
Weak scent (mechanism, not meaning):
data_handler,process_manager,item_controllerflag,result,status,value,infoManager,Helper,Util,Base
Graveyard names where scent goes to die: utils/, helpers/, common/,
shared/, misc/. These contain every kind of content, so they provide no
navigation signal. A new engineer looking for tax calculation logic will never
guess to check utils/helpers.py.
Import path as scent trail:
# Every token narrows the search — strong scent trail
from app.billing.invoices.line_items import calculate_tax
# Trail goes cold — what kind of util? what does it help with?
from utils.helpers import calculate_taxRenaming test: if you had to rename a module so a new engineer finds it on their first try, what would you call it? That's the right name.
Code Hygiene
No Unnecessary from __future__ import annotations
Only add it for genuine circular type references via TYPE_CHECKING. Modern
Python (3.10+) has X | Y and list[int] natively. See the TYPE_CHECKING
section in references/advanced-patterns.md for the legitimate use case.
Specific Exception Types
Never bare except: or except BaseException — these catch KeyboardInterrupt
and SystemExit. Never except Exception without justification. Look up which
exceptions the called code actually raises and catch those. When genuinely
warranted, add an inline comment explaining why.
# Bad
except Exception:
log.error("failed")
# Good
except KeyError:
...
# Also fine
except (KeyError, TypeError):
...
# Acceptable when justified with a comment
# Plugin loader must never crash the host process.
except Exception:
logger.exception("plugin failed to load")
# Also acceptable in rollback/teardown paths — must always re-raise
except Exception: # must rollback on any application exception, then re-raise
conn.rollback()
raiseLet None Propagate from Dict Access
Use .get("key") without a default. Providing "" or 0 as a fallback
masks missing keys — None is the honest signal that data is absent.
Supply a default only when None is a valid value in the dict and you
need to distinguish "missing" from "explicitly None."
# Bad — masks that "count" could be missing
count = data.get("count", 0)
# Good — let None propagate; handle it explicitly downstream
count = data.get("count")
# Acceptable — None is a valid value, so a default disambiguates
enabled = config.get("enabled", True)No Empty-String Defaults
"" is not "no value." Use None, or better, require the argument. Same
applies to [] and {} as sentinels. Same for model/dataclass fields — if
a field can be absent, type it as T | None = None, not str = "".
# Bad — "" is not "no value"
def find_user(name: str = "") -> User: ...
# Good
def find_user(name: str | None = None) -> User: ...
# Best — require it
def find_user(name: str) -> User: ...Docstring Discipline
Google-style docstrings. Two sub-rules:
- Never put type annotations in docstrings — types belong in PEP 484 annotations on the signature. Duplicating them creates noise and a second source of truth that drifts.
- Omit sections that restate what the signature already says. If
Argsnames and types tell the whole story, orReturnstype is self-explanatory, skip those sections.
# Bad — redundant types and sections
def fetch_user(user_id: int, timeout: float = 5.0) -> User:
"""Fetch a user by ID.
Args:
user_id (int): The user's ID.
timeout (float): Request timeout in seconds.
Returns:
User: The fetched user.
"""
# Good — only document what isn't obvious
def fetch_user(user_id: int, timeout: float = 5.0) -> User:
"""Fetch a user by ID.
Raises:
UserNotFoundError: If no user exists for the given ID.
"""Prefer Pathlib Methods Over String Manipulation
Use with_suffix, with_stem, with_name instead of string concatenation
or f-string path assembly. These methods handle edge cases (double
extensions, empty stems) that string manipulation silently gets wrong.
# Bad
output = str(input_path) + ".json"
output = f"{input_path.parent}/{input_path.stem}.json"
# Good
output = input_path.with_suffix(".json")
output = input_path.with_name("output.json")Imports at Module Top
All imports belong at the top of the file — never inside functions. Lazy imports hide dependencies and make it harder to see what a module depends on at a glance. Sort: stdlib, third-party, local.
# Bad
def process():
from pathlib import Path
...
# Good
from pathlib import Path
def process():
...Be Concise
- No docstrings that restate the signature.
- No comments that restate the code.
- No single-use type aliases.
- No unnecessary helper functions that are only called once and don't simplify.
References
| Reference | When to read it |
references/core-patterns.md | Full examples for type contracts (Protocols, ABCs, Mixins), data representation (frozen dataclasses, TypedDict, Enums, NamedTuple), error hierarchies, and context managers |
references/composition.md | Generator pipelines, itertools, functools (partial, singledispatch, cache, wraps), standalone functions vs methods |
references/advanced-patterns.md | Advanced typing (@overload, TypeGuard, generics, TYPE_CHECKING), structural pattern matching, immutability toolkit, async patterns, module organization and docstring conventions |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

