Python
29 / 29
Design Patterns
Classic patterns, written the Pythonic way — often shorter than the Java version.
Patterns ≠ rules
Patterns are vocabulary for solutions that keep appearing. In Python, many of them collapse into a function or a Protocol — don't import boilerplate you don't need.
1. Strategy
Swap the algorithm without changing the caller. In Python, functions are strategies.
def quick_sort(xs): ...
def merge_sort(xs): ...
def sort_with(xs, strategy=quick_sort):
return strategy(xs)
sort_with(data, strategy=merge_sort)2. Factory
Centralize object creation so callers don't depend on concrete classes.
def make_model(name: str):
return {
"lr": LogisticRegression(),
"rf": RandomForestClassifier(),
"xgb": XGBClassifier(),
}[name]
model = make_model(config["model"])3. Singleton
One instance app-wide. In Python, a module is already a singleton — use that first.
# settings.py
class _Settings:
db_url = os.environ["DB_URL"]
debug = False
settings = _Settings() # imported everywhere; one instance4. Observer (Pub/Sub)
class EventBus:
def __init__(self):
self._subs: dict[str, list[Callable]] = defaultdict(list)
def on(self, event, handler): self._subs[event].append(handler)
def emit(self, event, payload):
for h in self._subs[event]: h(payload)
bus = EventBus()
bus.on("order.placed", send_confirmation_email)
bus.on("order.placed", update_inventory)
bus.emit("order.placed", order)5. Adapter
Wrap an incompatible interface so it matches the one your code expects.
class StripeChargesAPI:
def create_charge(self, cents, card): ...
class PaymentGateway(Protocol):
def pay(self, amount: Money, method: PaymentMethod) -> Receipt: ...
class StripeAdapter:
def __init__(self, api: StripeChargesAPI): self.api = api
def pay(self, amount, method):
raw = self.api.create_charge(amount.cents, method.card)
return Receipt.from_stripe(raw)6. Decorator
Wrap behavior around a function — caching, retries, auth, metrics.
def retry(times=3):
def deco(fn):
@functools.wraps(fn)
def wrapper(*a, **kw):
for i in range(times):
try: return fn(*a, **kw)
except TransientError:
if i == times - 1: raise
time.sleep(2 ** i)
return wrapper
return deco
@retry(times=5)
def fetch_user(id): ...7. Repository
Hide persistence behind a Pythonic collection interface — testable, swappable.
class UserRepo(Protocol):
def get(self, id: int) -> User | None: ...
def add(self, user: User) -> None: ...
class SqlUserRepo: def get(self, id): ...
class InMemoryUserRepo: def get(self, id): ... # for testsWhen NOT to use a pattern
- You have one implementation and no plan for a second — YAGNI.
- The pattern adds more code than the problem it solves.
- You're copying Java/C# blueprints verbatim — Python usually has a one-liner.
The Pythonic rule
Reach for a Protocol + a function before reaching for a class hierarchy. Patterns should appear because the code needed them, not because you wanted to use one.