Design patterns in Python matters once your pages get past the basics. The useful ones, expressed the Python way. Patterns are shared names for recurring problems. They are not a checklist to implement in every project, and they are definitely not an excuse to build a UML diagram before you write ten lines of code.
Python already is the pattern
Many Gang-of-Four patterns collapse in Python because the language gives you first-class functions, duck typing, and simple modules. A “strategy” is often a callable passed as an argument. A “factory” is a function that returns the right class. Do not reach for classes when a function will do.
def apply_discount(base: float, rule) -> float:
return rule(base)
def ten_percent_off(amount: float) -> float:
return amount * 0.9
def flat_two_off(amount: float) -> float:
return max(0, amount - 2)
print(apply_discount(50, ten_percent_off))
Composition over inheritance
Deep inheritance trees are brittle. Prefer small objects that wrap or delegate to collaborators.
class ConsoleNotifier:
def send(self, message: str) -> None:
print(message)
class OrderService:
def __init__(self, notifier: ConsoleNotifier) -> None:
self.notifier = notifier
def place_order(self, item: str) -> None:
# business logic here
self.notifier.send(f"Order placed: {item}")
Dependency injection does not need a framework. Pass collaborators into __init__ or factory functions. Tests swap in fakes without subclassing half your app.
Registry and plugin style
A registry maps names to callables or classes. Flask blueprints, pytest plugins, and CLI subcommands all rhyme with this idea.
HANDLERS: dict[str, callable] = {}
def register(name: str):
def decorator(func):
HANDLERS[name] = func
return func
return decorator
@register("csv")
def export_csv(rows: list[dict]) -> str:
return "csv data"
@register("json")
def export_json(rows: list[dict]) -> str:
return '{"rows": ...}'
Context managers and the with pattern
Resource management is a pattern Python built into the language. Your own types can participate with __enter__ and __exit__, or the contextlib.contextmanager decorator for generator-style setup and teardown.
from contextlib import contextmanager
@contextmanager
def temp_env(key: str, value: str):
import os
old = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
if old is None:
os.environ.pop(key, None)
else:
os.environ[key] = old
Dataclasses for simple data carriers
When a object mostly holds data, a dataclass beats a hand-written __init__. Add methods when behaviour genuinely belongs there.
from dataclasses import dataclass
@dataclass(frozen=True)
class Address:
line1: str
city: str
postcode: str
def formatted(self) -> str:
return f"{self.line1}, {self.city} {self.postcode}"
Patterns to use sparingly
- Singleton – usually a module-level object or explicit app factory is clearer
- Abstract factory hierarchies – only when you truly swap whole families of implementations
- Visitor – rare in Python; often a dict dispatch or
matchstatement reads better
Name patterns when they help your team communicate. Skip them when a plain function and a good module layout tell the story on their own.

