Pythonic Patterns Best Practices
Rules for readable, maintainable Python structure - from EAFP to dependency wiring.
How to Use This List
- Apply during design reviews before adding new abstraction layers.
- Prefer one clear pattern per boundary - do not mix dispatch styles in the same module.
- Enforce with ruff, mypy/pyright, and architecture tests where possible.
A - Idioms & Readability
- Prefer EAFP when success is common. Linear happy paths beat nested guards; catch specific exceptions.
- Use
withfor acquire/release pairs. Files, locks, and transactions need guaranteed teardown. - Reach for dict dispatch before elif chains grow past five branches. Register plugins explicitly.
- Name factory functions
thing_from_env/build_thing. Callers recognize composition-root helpers. - Readability counts over cleverness. If a junior cannot trace it in five minutes, simplify.
B - Structure & Coupling
- Inject dependencies through constructors. No hidden
get_db()singletons in service methods. - Depend on protocols, wire concretes at the root. Application code stays testable and swappable.
- Use null objects for optional no-op collaborators. Not for required production dependencies.
- Publish domain events through a bus interface. Handlers stay small; failures logged per handler.
- Split god objects by responsibility before adding the tenth method. Extract protocols first.
C - Defaults & Safety
- Never use mutable default arguments.
Nonesentinel plus fresh container inside the function. - Use explicit sentinels when
Noneis valid data. Documentischecks forUNSETobjects. - Validate builders in
build(), not in setters. Keep partial builder state internal. - Avoid import-time I/O and network. Startup hooks and lifespan handlers own side effects.
- Catch specific exceptions; never bare
except. Log context; re-raise unless suppression is contractual.
D - Abstraction Discipline
- Rule of three before extracting interfaces. Wait for three real duplications, not imagined futures.
- Prefer functions and dataclasses over deep inheritance. Composition and dispatch cover most Python cases.
- Replace boolean flag sprawl with enums or strategies. Modes are types, not four
True/Falseparams. - Keep public APIs explicit - minimal
**kwargs. TypedDict or Pydantic at HTTP/CLI boundaries. - Document valid registry keys and event names.
StrEnumorLiteralfor static checking.
E - Review & Evolution
- Scan for anti-patterns in high-churn modules quarterly. Mutable defaults and swallowed errors first.
- One integration test per dispatcher/registry. Unknown keys and types fail with clear messages.
- Refactor extract-by-protocol in small PRs. Facade delegates until callers migrate.
- Match pattern to problem size. Scripts stay simple; services get injection and events.
- Link ADRs when choosing bus vs direct calls. Future readers know the trade-off accepted.
FAQs
What is the highest-impact practice for new code?
Constructor injection plus protocols - tests improve immediately without frameworks.
When should I skip patterns entirely?
One-off scripts under ~100 lines: plain functions, no bus, no builder.
How do I enforce dict dispatch?
Central registry module, typed keys as StrEnum, and tests that fail on unknown keys.
Are class-based context managers required?
No. @contextmanager covers most cases; classes when enter/exit state is substantial.
How does this relate to PEP 20?
Explicit, readable, flat - these practices operationalize the Zen of Python in architecture.
Should every service emit events?
Only for boundaries other modules subscribe to. Internal steps stay direct calls.
How do I migrate elif chains safely?
Introduce dict mapping existing branches; keep elif delegating until coverage matches; delete branches.
Does FastAPI change DI advice?
Depends() is parameter injection at routes - same principle, framework-managed scope per request.
What lint rules help most?
ruff B006 (mutable defaults), E722 (bare except), and pyright on protocol implementations.
How do I teach patterns to juniors?
Start with Pythonic Patterns Basics, then review one anti-pattern per sprint.
Related
- Pythonic Patterns Basics - hands-on examples
- Common Anti-Patterns - mistakes to avoid
- EAFP vs LBYL - exception style
- Dependency Injection in Python - wiring detail
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.