Pythonic Patterns Basics
10 examples to get you started with Pythonic Patterns - 7 basic and 3 intermediate.
Prerequisites
- Python 3.14.0 installed.
- Read
import thisfor the Zen of Python - readability counts.
Basic Examples
1. EAFP Key Lookup
Try the operation; handle KeyError for missing keys.
def get_score(scores: dict[str, int], player: str) -> int:
try:
return scores[player]
except KeyError:
return 0- EAFP keeps the happy path unindented.
- Use
.get()when a dict default is enough. - Do not catch overly broad exceptions around simple lookups.
Related: EAFP vs LBYL - when to guard instead
2. Context Manager for Files
Guarantee cleanup with with.
from pathlib import Path
path = Path("notes.txt")
path.write_text("hello", encoding="utf-8")
with path.open(encoding="utf-8") as f:
print(f.read())- Files close even when reading raises.
- Prefer
pathlibover manual path strings. - Specify
encodingfor text files on all platforms.
Related: Context Managers as a Pattern - custom managers
3. Strategy Dict
Replace if/elif with a registry of callables.
FORMATTERS = {
"json": lambda data: str(data),
"upper": lambda data: str(data).upper(),
}
def format_value(kind: str, data) -> str:
return FORMATTERS[kind](data)- New formats add one dict entry, not another branch.
- Validate keys and raise clear errors for unknown kinds.
- Name functions when logic grows beyond lambdas.
Related: The Strategy & Dispatch Patterns - singledispatch too
4. Factory Function
Hide concrete class choice behind one constructor function.
from dataclasses import dataclass
@dataclass
class ConsoleLogger:
def info(self, msg: str) -> None:
print(msg)
def logger_from_env(debug: bool) -> ConsoleLogger:
return ConsoleLogger()- Callers depend on the factory, not import paths of implementations.
- Factories belong at the composition root or next to product types.
- Keep factories free of network side effects.
Related: Factory & Builder Patterns - builders for complex objects
5. Simple Event Bus
Decouple publishers and subscribers with topics.
from collections import defaultdict
bus: dict[str, list] = defaultdict(list)
bus["user.created"].append(lambda e, p: print("audit", p))
bus["user.created"][0]("user.created", {"id": 1})- Subscribers should be fast; offload heavy I/O elsewhere.
- Copy handler lists during emit to avoid mutation bugs.
- Inject a bus object instead of using a global singleton.
Related: Observer & Pub/Sub - production-shaped bus
6. Constructor Injection
Pass dependencies explicitly for testability.
class Greeter:
def __init__(self, salutation: str) -> None:
self._salutation = salutation
def greet(self, name: str) -> str:
return f"{self._salutation}, {name}!"- Dependencies visible in the constructor signature.
- Tests pass alternate implementations without monkeypatching.
- Avoid constructing I/O clients inside service methods.
Related: Dependency Injection in Python - protocols and wiring
7. Sentinel for Unset Values
Distinguish "not passed" from None.
UNSET = object()
def connect(timeout=UNSET):
if timeout is UNSET:
timeout = 30.0
return timeout- Compare sentinels with
is, not==. - Document three-state semantics when
Noneis valid data. - Pydantic models can use
model_fields_setinstead for APIs.
Related: Null Object & Sentinels - null objects
Intermediate Examples
8. singledispatch Serialization
Add type-specific serializers without editing the core function.
from functools import singledispatch
@singledispatch
def to_repr(obj) -> str:
raise TypeError(type(obj))
@to_repr.register(int)
def _(n: int) -> str:
return f"int:{n}"
print(to_repr(42))- Register new types in new modules for plugin layouts.
- Base function should raise clear
TypeErrorfor unsupported types. - Prefer dict strategies for string-keyed dispatch instead.
Related: The Strategy & Dispatch Patterns - full comparison
9. Fluent Builder with Validation
Assemble a config object step-by-step; validate at build().
class QueryBuilder:
def __init__(self) -> None:
self._filters: list[str] = []
def where(self, clause: str) -> "QueryBuilder":
self._filters.append(clause)
return self
def build(self) -> str:
if not self._filters:
raise ValueError("need at least one filter")
return " AND ".join(self._filters)
sql = QueryBuilder().where("id > 0").where("active").build()
print(sql)- Builders suit many optional fields and ordering constraints.
- Return
selffor chaining; validate invariants once inbuild(). - Simple dataclasses may replace builders when fields are few.
Related: Factory & Builder Patterns - factory vs builder
10. Null Object Logger
Skip branching when logging is disabled.
class NullLogger:
def info(self, msg: str) -> None:
pass
def job(logger=NullLogger()):
logger.info("run")- Null objects implement the real interface with no-ops.
- Do not use null objects to hide missing required dependencies in production.
- Prefer explicit injection over mutable default instances in libraries.
Related: Common Anti-Patterns - mutable defaults and god objects
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+.