The Design Patterns book — the 1994 “Gang of Four” catalogue — is the most influential software book most people have never actually needed. It gave a generation a shared vocabulary: say “Observer” and a room of engineers pictures the same thing. That vocabulary is genuinely valuable. But the book was written in and for C++, a language with no first-class functions, no closures, no duck typing, and a type system that fights you. A large fraction of the twenty-three patterns are elaborate ceremonies for doing something those languages could not do directly — and Python can.
Here is the uncomfortable truth this lesson is built around, stated once, plainly:
# The "Strategy pattern": an interface, two concrete classes, a context that holds one.
class Order:
def __init__(self, shipping): self.shipping = shipping
def cost(self, w): return self.shipping.cost(w) # delegate to the strategy
# The same thing in Python:
order.shipping = by_weight(20) # a strategy is just a function you pass in
Both are “the Strategy pattern.” One is four classes and an abstract base; the other is a function passed into a slot. When a pattern is mostly boilerplate, the boilerplate is not the pattern — the intent is. Python keeps the intent and throws the boilerplate away, because a function is an object, a dict is dispatch, a module is a singleton, and any object with the right methods is the interface. Peter Norvig made this point in 1996: sixteen of the twenty-three GoF patterns are “invisible or simpler” in a language with first-class functions and dynamic typing. Python is exactly that language.
This lesson teaches each pattern twice: the real, classic structure (so you recognise it in Java, in a codebase, in an interview) and the pythonic form it collapses to (so you write the smaller, better version). For every one you get the problem it solves, a minimal example that actually runs, the pythonic alternative, and — the part that matters most — when the full ceremony genuinely earns its keep, because sometimes it does.
Why this matters
You will meet patterns from two directions, and both go wrong without this lesson.
From the interview and the textbook, you will be asked to “implement the Observer pattern” or “use a Factory here,” and if you have only ever seen the Java form you will write four classes where Python wanted a function — and a good reviewer will quietly mark you down for it. The pattern vocabulary is real and worth knowing; reciting the implementation from a language that isn’t yours is not.
From the codebase, you will inherit — or worse, write — a NotificationManager that is a 2,000-line God object, a SingletonConfig that makes every test flaky, a five-level inheritance tree that could have been a list of functions, an AbstractWidgetFactoryProvider that constructs one kind of widget. This is cargo-cult engineering: applying the ritual of a pattern without the problem that justifies it. The GoF book itself warns against this on page 1, and it is the single most common way patterns hurt a Python codebase. Patterns are a vocabulary for a problem you already have, not a checklist to apply pre-emptively.
The mental model to carry through the whole lesson is this. A design pattern is an answer to a question of the form “how do I vary X without rewriting Y?” — vary the algorithm without rewriting the caller (Strategy), vary the created type without rewriting the constructor (Factory), vary who-reacts without rewriting the source (Observer). In a language with first-class functions, the answer to almost all of those is the same: pass the varying part in as an argument. A function is the smallest possible unit of “behaviour you can pass around,” and most patterns are scaffolding built to move behaviour around in languages that can’t pass a function. Keep asking “what is varying, and can I just pass it in?” and you will reinvent the pythonic form of half the catalogue on your own.
The GoF’s twenty-three patterns fall into three families, and it helps to hold the whole map in view before zooming in — because the family tells you, in advance, roughly what the pattern will collapse to in Python:
| GoF family | What it varies | Example patterns | Typical Python status |
|---|---|---|---|
| Creational | How objects are made | Factory Method, Abstract Factory, Singleton, Builder, Prototype | A function, a dict, a registry, or a module |
| Structural | How objects are composed | Adapter, Decorator, Proxy, Facade, Composite, Bridge, Flyweight | Some survive (Adapter); many become a wrapping function or __getattr__ |
| Behavioural | How responsibility and behaviour flow | Strategy, Observer, Command, Template Method, Iterator, State, Visitor, Chain, Mediator, Memento | Mostly “pass a function”; Iterator and resource-management became syntax |
The behavioural row is the one that all but vanishes, because behaviour is precisely what a first-class function is. Keep that map open as we walk the patterns one by one.
One more framing, because it prevents the biggest mistake. The goal is never “use a pattern.” The goal is code that is easy to change, easy to test, and easy to read. A pattern is worth its cost only when it buys one of those three against a change you can actually name. If you cannot name the change you are protecting against, you are not designing — you are decorating, and the decoration will cost the next reader real time.
Strategy: interchangeable algorithms
The problem: you have one operation whose algorithm needs to vary — how shipping is priced, how a list is sorted, how a payment is retried — and you do not want a growing if/elif ladder in the caller, one branch per algorithm, edited by everyone.
The classic OO Strategy defines an interface, one class per algorithm, and a context that holds a strategy and delegates to it:
from abc import ABC, abstractmethod
from decimal import Decimal
class ShippingStrategy(ABC):
@abstractmethod
def cost(self, weight_kg: float) -> Decimal: ...
class FlatRate(ShippingStrategy):
def cost(self, weight_kg):
return Decimal("50")
class ByWeight(ShippingStrategy):
def __init__(self, per_kg):
self.per_kg = Decimal(str(per_kg))
def cost(self, weight_kg):
return self.per_kg * Decimal(str(weight_kg))
class Order:
def __init__(self, weight_kg, shipping: ShippingStrategy):
self.weight_kg = weight_kg
self.shipping = shipping # the context HOLDS a strategy
def shipping_cost(self):
return self.shipping.cost(self.weight_kg) # ... and DELEGATES to it
print(Order(3, FlatRate()).shipping_cost()) # => 50
print(Order(3, ByWeight(20)).shipping_cost()) # => 60
o = Order(3, FlatRate())
o.shipping = ByWeight(20) # swap the algorithm at runtime
print(o.shipping_cost()) # => 60
That works, and the runtime swap is the whole point: Order never contains a single if about shipping, and adding a FreeShipping strategy touches zero existing code. This is genuinely good design. But look at FlatRate — a class, an ABC, a method — to hold one function’s worth of behaviour. In Python, an object with one method is a function wearing a costume.
The pythonic Strategy is a function you pass in:
from decimal import Decimal
def flat_rate(weight_kg):
return Decimal("50")
def by_weight(per_kg): # returns a configured strategy (a closure)
def cost(weight_kg):
return Decimal(str(per_kg)) * Decimal(str(weight_kg))
return cost
class Order:
def __init__(self, weight_kg, shipping): # `shipping` is just a callable
self.weight_kg = weight_kg
self.shipping = shipping
def shipping_cost(self):
return self.shipping(self.weight_kg) # call it directly — no .cost()
print(Order(3, flat_rate).shipping_cost()) # => 50
print(Order(3, by_weight(20)).shipping_cost()) # => 60
print(Order(3, lambda w: Decimal("0")).shipping_cost()) # => 0 free shipping, inline
Same behaviour, same runtime swappability, a third of the code, and you can pass a lambda for a one-off without declaring a class at all. by_weight(20) is a closure — a function carrying its per_kg — which is the functional twin of “a strategy object with configuration.” (Closures are covered in depth in Functional Python; here they are the engine that makes the pattern vanish.)
The diagram below is the whole pattern in one picture. Read it left to right, following a single call: the Caller invokes the Context, which holds a strategy reference and delegates through one call site; behind that seam sit interchangeable algorithms — as OO objects, or, pythonically, as a plain function or closure — and both produce the identical result without a single if kind == ... branch in the caller.
The six badges mark the load-bearing ideas: the Context delegates through one call site (1) to interchangeable algorithms (2) that are injected at runtime (3); in Python those collapse to just a function (4); both paths yield the same result (5); and the whole thing exists to replace the if/elif anti-pattern (6) that would otherwise grow in the caller.
When does the class form earn its keep over a bare function? When the strategy carries state or configuration you want to inspect, when it needs a good repr / equality / serialisation (a frozen dataclass strategy can be compared and stored in a config table — see domain models), or when it groups several related methods rather than one. A retry policy with should_retry(), backoff() and max_attempts is a real object; a single pricing rule is a function.
| Aspect | Classic OO Strategy | Pythonic (pass a function) |
|---|---|---|
| Interface | An ABC with @abstractmethod |
The function’s call signature — nothing to declare |
| One algorithm | A class + a method | A def (or a lambda) |
| Configuration | __init__ stores it on the instance |
A closure captures it (by_weight(20)) |
| Swap at runtime | ctx.strategy = OtherStrategy() |
ctx.strategy = other_fn |
| A one-off | Still a whole class | An inline lambda |
| Comparable / serialisable | Yes, if you write __eq__ |
Functions aren’t; use a frozen-dataclass strategy if needed |
| Lines of code | ~4× | 1× |
| Reach for it when | The strategy has state + several methods | The strategy is one operation (the common case) |
| Strategy in the wild | The “strategy” you pass |
|---|---|
sorted(data, key=...) |
A key function — the canonical pythonic Strategy |
sorted(data, key=..., reverse=True) |
Ordering strategy as a flag + function |
defaultdict(list) |
A factory strategy for missing keys |
max(items, key=len) |
The comparison strategy |
re.sub(pat, repl, s) where repl is a function |
A per-match replacement strategy |
list.sort(key=attrgetter("age")) |
operator.attrgetter as a reusable strategy |
That first row is the point: Python’s own standard library implements Strategy hundreds of times a day and never once defines a SortStrategy class. It passes a function.
Factory, Factory Method and Abstract Factory
The problem: you need to create an object, but which concrete type depends on data known only at runtime — a config string, a file extension, a message type field — and you do not want if/elif construction ladders scattered across the code.
The GoF splits this into three patterns of increasing ceremony. It helps to see them named:
| GoF factory pattern | What it is | Ceremony |
|---|---|---|
| Simple Factory (not officially GoF) | One function/method that returns one of several types by argument | Low |
| Factory Method | A method on a base class that subclasses override to choose the type | Medium — a class hierarchy |
| Abstract Factory | An object whose several methods each create a matching family of products | High — a hierarchy of factories |
Here is Factory Method in full classic form — a factory hierarchy that mirrors the product hierarchy:
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, msg): ...
class EmailNotifier(Notifier):
def send(self, msg): return f"email: {msg}"
class SmsNotifier(Notifier):
def send(self, msg): return f"sms: {msg}"
class NotifierFactory(ABC):
@abstractmethod
def create(self) -> Notifier: ...
class EmailFactory(NotifierFactory):
def create(self): return EmailNotifier()
class SmsFactory(NotifierFactory):
def create(self): return SmsNotifier()
def notify(factory: NotifierFactory, msg):
return factory.create().send(msg)
print(notify(EmailFactory(), "hi")) # => email: hi
print(notify(SmsFactory(), "hi")) # => sms: hi
Count the classes: two products, two factories, two abstract bases — six types to send a string two ways. In Java, where a function cannot be passed and a class cannot be created from a name at runtime, this is necessary. In Python it is theatre, because a class is already a first-class object you can pass, store and call.
Pythonic factory, level 1 — a plain function:
def make_notifier(kind):
if kind == "email":
return EmailNotifier()
if kind == "sms":
return SmsNotifier()
raise ValueError(f"unknown notifier: {kind!r}")
print(make_notifier("email").send("hi")) # => email: hi
make_notifier("carrier-pigeon")
# ValueError: unknown notifier: 'carrier-pigeon'
A single function replaces the entire factory hierarchy. For a handful of fixed types this is the correct answer — do not out-engineer it.
Pythonic factory, level 2 — dict dispatch. When the if/elif ladder grows, replace it with a mapping from name to class. The class is the value.
NOTIFIERS = {"email": EmailNotifier, "sms": SmsNotifier}
def make_notifier(kind):
try:
return NOTIFIERS[kind]() # look up the class, then call it
except KeyError:
raise ValueError(f"unknown notifier: {kind!r}") from None
print(make_notifier("email").send("hi")) # => email: hi
The from None suppresses the KeyError context so the traceback shows only your clean ValueError rather than a confusing “during handling of the above exception” chain. Adding a type is now one dict entry, and the mapping is data you can iterate, log, or load from config.
Pythonic factory, level 3 — a real plugin registry with a decorator. This is the pattern worth mastering, because it is how Flask routes, pytest plugins, Click commands and Django admin registrations all work. A decorator registers each class at definition time, so adding a plugin never edits a central list:
_REGISTRY = {}
def register(name):
def deco(cls):
if name in _REGISTRY:
raise ValueError(f"notifier {name!r} already registered")
_REGISTRY[name] = cls
return cls # return cls unchanged — just record it
return deco
@register("email")
class Email:
def send(self, msg): return f"email: {msg}"
@register("sms")
class Sms:
def send(self, msg): return f"sms: {msg}"
@register("slack")
class Slack:
def send(self, msg): return f"slack: {msg}"
def create(name, *args, **kwargs):
try:
return _REGISTRY[name](*args, **kwargs)
except KeyError:
raise ValueError(f"unknown notifier {name!r}; "
f"have {sorted(_REGISTRY)}") from None
print(sorted(_REGISTRY)) # => ['email', 'slack', 'sms']
print(create("slack").send("deploy done")) # => slack: deploy done
create("fax")
# ValueError: unknown notifier 'fax'; have ['email', 'slack', 'sms']
The decorator is the Abstract Factory’s registration, done in one line per plugin with zero factory classes. A new notification channel is a new file with @register("...") on top — the core never changes, which is exactly the open/closed goal the GoF hierarchy chases with six classes.
This is not a toy: the decorator registry is one of the most widely used patterns in the Python ecosystem, and recognising it in a library’s source is a genuine skill. You have almost certainly used all of these:
| Library | Registration syntax | What the registry maps |
|---|---|---|
| Flask / FastAPI | @app.route("/path") / @app.get(...) |
URL rule → view function |
| pytest | @pytest.fixture / entry-point plugins |
Name → fixture / hook |
| Click | @cli.command() |
Command name → callback |
| Django | @admin.register(Model) |
Model → admin class |
functools |
@fn.register (singledispatch) |
Argument type → implementation |
atexit |
@atexit.register |
— → cleanup callback to run at exit |
| Celery | @app.task |
Task name → callable for the worker |
Every one of these is “a decorator writing into a dict” — the factory registry you just built, at production scale.
Two more pythonic factories deserve a mention because they cover the remaining cases:
from functools import singledispatch
@singledispatch # dispatch on the TYPE of the argument
def to_json(x):
raise TypeError(f"no rule for {type(x).__name__}")
@to_json.register
def _(x: int):
return str(x)
@to_json.register
def _(x: list):
return "[" + ",".join(to_json(i) for i in x) + "]"
print(to_json(5)) # => 5
print(to_json([1, 2, 3])) # => [1,2,3]
functools.singledispatch is a type-based factory: it picks the implementation from the argument’s class — the pythonic answer to “Abstract Factory keyed on product type.” And for a fixed, closed set of choices, an Enum is the right registry:
from enum import Enum
class Channel(Enum):
EMAIL = "email"
SMS = "sms"
print(Channel("email"), Channel.EMAIL.value) # => Channel.EMAIL email
Channel("fax")
# ValueError: 'fax' is not a valid Channel
| Factory need | Pythonic form | Why |
|---|---|---|
| A few fixed types | A plain function with if/elif |
Smallest thing that works |
| Growing set, known at import | dict mapping name → class |
Data-driven; add a row |
| Open/extensible plugins | @register(...) decorator registry |
New plugin = new file, core untouched |
| Choose by argument type | functools.singledispatch |
Type-keyed dispatch, built in |
| Fixed, closed vocabulary | Enum |
Validates membership for free |
| Family of related products | A module or a frozen dataclass of callables | A module groups related factories naturally |
When is the classic factory hierarchy actually right in Python? Rarely — but when a factory itself needs to carry configuration and state (a ConnectionFactory holding a pool, credentials and retry policy, with several create_* methods), a small factory class is reasonable. The tell is state: if the factory is stateless, it wants to be a function or a dict, not a class.
Observer and pub/sub
The problem: something happens in one place (an order is placed, a sensor reads a value, a file changes) and an open-ended set of other parts of the system need to react — send an email, update a metric, invalidate a cache — without the source knowing who they are or how many there will be.
The classic OO Observer defines an Observer interface with update(), and a Subject that keeps a list of observers and notifies each:
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temp): ...
class Display(Observer):
def __init__(self, name): self.name = name
def update(self, temp): print(f" {self.name}: {temp}C")
class Subject:
def __init__(self):
self._observers = []
def attach(self, obs): self._observers.append(obs)
def detach(self, obs): self._observers.remove(obs)
def notify(self, temp):
for obs in self._observers:
obs.update(temp)
s = Subject()
s.attach(Display("phone"))
s.attach(Display("watch"))
s.notify(21)
# => phone: 21C
# => watch: 21C
Again, Display is a class with one method — update — so again it wants to be a function. The pythonic Observer is a list of callbacks:
class Sensor:
def __init__(self):
self._callbacks = []
def subscribe(self, fn):
self._callbacks.append(fn)
return fn # return fn so it also works as a decorator
def emit(self, temp):
for fn in self._callbacks:
fn(temp)
sensor = Sensor()
sensor.subscribe(lambda t: print(f" log: {t}C"))
@sensor.subscribe # decorator registration reads beautifully
def alarm(temp):
if temp > 30:
print(f" ALARM: {temp}C")
sensor.emit(21) # => log: 21C
sensor.emit(35) # => log: 35C
# => ALARM: 35C
An observer is any callable of the right shape. return fn from subscribe is a small, idiomatic touch: it lets the method double as a decorator, so @sensor.subscribe above the function is the subscription — the same trick Flask’s @app.route uses.
When many event types and many subscribers meet, the pattern grows into an event bus (pub/sub): a central broker keyed by event name, so publishers and subscribers never reference each other at all.
from collections import defaultdict
class EventBus:
def __init__(self):
self._subs = defaultdict(list)
def on(self, event, fn):
self._subs[event].append(fn)
return fn
def off(self, event, fn):
self._subs[event].remove(fn)
def emit(self, event, **data):
for fn in list(self._subs[event]): # iterate a COPY: a handler may unsubscribe
fn(**data)
bus = EventBus()
def on_order(order_id, total, **_):
print(f" invoice for {order_id}: {total}")
def on_order_metrics(order_id, **_):
print(f" metric: order_placed {order_id}")
bus.on("order.placed", on_order)
bus.on("order.placed", on_order_metrics)
bus.emit("order.placed", order_id="ORD-1", total="INR 484.00")
bus.off("order.placed", on_order_metrics)
print(" -- after off --")
bus.emit("order.placed", order_id="ORD-2", total="INR 99.00")
invoice for ORD-1: INR 484.00
metric: order_placed ORD-1
-- after off --
invoice for ORD-2: INR 99.00
Three details in that EventBus are not decoration — they are the difference between a toy and a tool:
| Detail | Why it is there | The bug it prevents |
|---|---|---|
defaultdict(list) |
on() and emit() never check whether the event key exists |
KeyError on the first subscribe/emit of a new event |
**data payload |
Publisher and subscriber signatures stay decoupled; each handler takes **_ |
A new payload field breaking every existing handler |
for fn in list(self._subs[event]) |
Iterates a copy of the handler list | RuntimeError: ... changed size during iteration when a handler unsubscribes mid-emit |
off() provided at all |
Lets a subscriber be released | A memory leak — the bus pins subscribers forever (see troubleshooting) |
return handler from on() |
Lets on double as a decorator |
Boilerplate — you’d write the handler then subscribe it on a second line |
That third row is the classic Observer bug, and it is invisible until a handler removes itself during a notification.
| Observer scale | Pythonic form | Use when |
|---|---|---|
| One event, a few reactors | A list of callbacks + emit() |
A single subject, in-process |
| One event, decorator-registered | subscribe returns fn; @subject.subscribe |
You want registration to read declaratively |
| Many events, many subscribers | An EventBus keyed by event name |
Publishers and subscribers must not know each other |
| Cross-process / durable | A real broker (Redis pub/sub, Kafka, RabbitMQ) | Events cross machines or must survive a crash |
| Attribute changed → react | property setter fires callbacks, or __set_name__ descriptors |
You are observing state, not events |
When is the class-based Observer interface worth it? When observers are stateful objects that also do other things — a CacheInvalidator that reacts to twelve event types and holds a connection — grouping those reactions as methods on a class is cleaner than twelve loose functions. But the subject almost never needs a base class; a list of callables is enough. And note the memory trap that the off() method exists to prevent, covered in troubleshooting below: a subject holds strong references to its subscribers, so a subscriber that never unsubscribes never dies.
Singleton: usually a module, occasionally a mistake
The problem the pattern claims to solve: exactly one instance of something (a config, a connection pool, a logger) shared everywhere.
The pythonic answer, first, because it is almost always the right one: use a module. A Python module is imported once and cached in sys.modules; every subsequent import returns the same object. Module-level state is a singleton, enforced by the interpreter, with zero boilerplate:
import sys
print(sys.modules["sys"] is sys) # => True the second import returns the SAME object
So the idiomatic “singleton config” is just a module:
# settings.py
DEBUG = False
_cache = {}
def get(key): return _cache.get(key)
def set(key, value): _cache[key] = value
# anywhere.py
import settings
settings.DEBUG = True # every importer sees this — it is one shared object
No class, no __new__, no metaclass, no thread-safety dance. The module is the single shared namespace. For the overwhelming majority of “I need one shared thing,” this is the answer, and reaching for a Singleton class instead is the first sign of a Java accent.
| Module-as-singleton gives you | Watch out for |
|---|---|
One shared object, enforced by sys.modules caching |
It is still global mutable state — the same test-isolation risk applies |
Zero boilerplate — no __new__, metaclass, or lock |
Import-time side effects (opening a DB on import) make imports slow and tests heavy |
| Lazy, thread-safe first import (the import lock handles it) | Circular imports if the module reaches back into its importers |
| Functions and module-level state, testable in isolation | Reassigning module.x from many places is as tangled as any global |
The caveats are real, which is why the best answer for a shared collaborator is usually not even a module singleton but injection — construct the object once and pass it down. But when you genuinely want process-wide constants or a small shared helper, a module beats every class-based singleton on this list.
But you will meet the class-based forms, so here they are. Via __new__:
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a, b = Config(), Config()
print(a is b) # => True
Via a metaclass (the “cleanest” OO form, reusable across classes):
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Registry(metaclass=SingletonMeta):
def __init__(self):
self.items = []
r1, r2 = Registry(), Registry()
print(r1 is r2) # => True
r1.items.append("x")
print(r2.items) # => ['x'] they are the same object — shared state
Via Borg / Monostate (many instances, one shared __dict__ — a cult favourite that is rarely a good idea):
class Borg:
_shared = {}
def __init__(self):
self.__dict__ = self._shared # all instances share one attribute dict
x, y = Borg(), Borg()
x.value = 42
print(y.value, x is y) # => 42 False different objects, identical state
Every one of these has a testing problem, and it is not academic. A singleton is global mutable state, and global mutable state leaks between tests. Watch two tests that each pass alone but not together:
class Counter(metaclass=SingletonMeta):
def __init__(self):
self.n = 0
def test_a():
c = Counter(); c.n += 1
assert c.n == 1
def test_b():
c = Counter(); c.n += 1 # SAME instance as test_a — n is already 1
assert c.n == 1
test_a() # passes
test_b() # AssertionError: expected 1, got 2
test_b fails with got 2 because Counter() returned the instance test_a already mutated. The tests are correct; the singleton is poisoning them. Now every test needs teardown that reaches into SingletonMeta._instances and clears it — global reset code, run in a specific order, exactly the fragility good tests avoid. Dependency injection (next section but one) is the cure: pass the shared thing in, and each test passes a fresh one.
The __new__ form has a second, subtler trap worth seeing once, because it silently corrupts state:
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
print(" __init__ ran") # runs on EVERY Config() call, not once
self.data = {}
a = Config(); a.data["x"] = 1
b = Config() # __init__ runs AGAIN and wipes data
print(a is b, a.data) # => True {} your data is gone
__new__ returns the cached instance, but Python still calls __init__ on it every time — so self.data = {} resets the object on the second construction. a.data is empty despite a is b. This is a real bug people ship, and it is one more reason the module form — which has no __init__ to re-run — is the sane default.
| Singleton form | One instance? | Testing pain | Verdict |
|---|---|---|---|
| A module | Yes (interpreter-enforced) | Low — but still global state | Default. Use this. |
__new__ guard |
Yes | High + __init__ re-runs every call |
Avoid; the re-run bug is easy to hit |
| Metaclass | Yes, reusable | High — must clear _instances per test |
Only if you truly need many singleton classes |
| Borg / Monostate | No (many objects, shared state) | High — shared __dict__ leaks |
Almost never; surprises readers |
| Injected dependency | As many as you choose | Low — pass a fresh one per test | The design that scales. |
The honest summary: “I need one of these” is usually “I need to not construct twelve of these,” and the answer to that is to construct one at the top of your program and pass it down — not to make the class refuse to be built twice. A Singleton that enforces oneness is a global in a costume, and globals are the thing your tests hate most.
Adapter, Decorator, Command, Template Method
Four “wrap or defer behaviour” patterns, each of which collapses in Python — but along different lines, so it is worth seeing them side by side.
Adapter — make an incompatible interface fit
The problem: you have code that expects one interface, and an object (a library, a legacy class) that offers a different one. An Adapter wraps the incompatible object and exposes the interface your code wants.
class LegacyThermometer:
def fetch_celsius_times_ten(self): # an awkward legacy API
return 215 # 21.5C, scaled by 10
class ThermometerAdapter:
def __init__(self, legacy):
self._legacy = legacy # HAS-A the adaptee (composition)
def read(self): # the interface OUR code wants
return self._legacy.fetch_celsius_times_ten() / 10
def show(source): # depends only on .read()
print(f" {source.read()}C")
show(ThermometerAdapter(LegacyThermometer())) # => 21.5C
The Adapter is one of the patterns that stays useful in Python, because the mismatch is real: your code calls .read(), the library offers .fetch_celsius_times_ten(), and something must bridge them. But when the target interface is a single method, the adapter is often just a function:
def read_celsius(legacy):
return legacy.fetch_celsius_times_ten() / 10
print(read_celsius(LegacyThermometer())) # => 21.5
The class form earns its keep when the target interface has several methods, or when you need the adapter to be a drop-in object passed where the real type is expected. The trap — see troubleshooting — is an adapter that leaks: it forwards the clean method but also exposes the adaptee’s ugly one, so callers start depending on fetch_celsius_times_ten through the adapter and the abstraction rots.
Decorator pattern vs Python’s @decorator — related, not the same
This is the most confused pair of names in Python, so be precise. The GoF Decorator pattern wraps an object to add responsibilities while keeping the same interface, and the wrappers stack at runtime:
class Coffee:
def cost(self): return 100
def desc(self): return "coffee"
class MilkDecorator: # wraps a Coffee, exposes the same interface
def __init__(self, wrapped): self._wrapped = wrapped
def cost(self): return self._wrapped.cost() + 20
def desc(self): return self._wrapped.desc() + " + milk"
class SugarDecorator:
def __init__(self, wrapped): self._wrapped = wrapped
def cost(self): return self._wrapped.cost() + 5
def desc(self): return self._wrapped.desc() + " + sugar"
drink = SugarDecorator(MilkDecorator(Coffee())) # stack wrappers at runtime
print(f" {drink.desc()} = {drink.cost()}") # => coffee + milk + sugar = 125
Python’s @decorator syntax wraps a function or class at definition time:
import functools
def timed(fn):
@functools.wraps(fn) # preserve the wrapped function's name/doc
def wrapper(*args, **kwargs):
print(f" calling {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
@timed
def add(a, b): return a + b
print(add(2, 3)) # => calling add
# => 5
print(add.__name__) # => add (thanks to functools.wraps)
They share a family resemblance — both “wrap X and preserve X’s interface” — but they operate on different things (objects vs callables) at different times (runtime composition vs definition-time transformation). The GoF version is for adding behaviour to an object you can stack; the @ version is for transforming a function or class where it is defined. (Decorators as a language feature go far deeper.) Say “the Decorator pattern” and you mean coffee-and-milk; say “a decorator” and you mean @timed. Knowing the difference is itself an interview question.
Command — encapsulate an action as an object
The problem: you want to treat “an action to perform later” as a value — to queue it, log it, undo it, or hand it to a button. The Command pattern wraps the action (and its reverse) in an object:
class Command:
def __init__(self, do, undo):
self.do, self.undo = do, undo
class Editor:
def __init__(self):
self.text = ""
self.history = []
def run(self, cmd):
cmd.do()
self.history.append(cmd)
def undo(self):
self.history.pop().undo()
ed = Editor()
def append(s):
n = len(s)
return Command(
do=lambda: setattr(ed, "text", ed.text + s),
undo=lambda: setattr(ed, "text", ed.text[:-n]),
)
ed.run(append("hello "))
ed.run(append("world"))
print(repr(ed.text)) # => 'hello world'
ed.undo()
print(repr(ed.text)) # => 'hello '
Command needs undo here, so a small object holding do/undo pays for itself. But the simplest Command — “run this later, with these arguments baked in” — is a closure or functools.partial, no class at all:
from functools import partial
def greet(name, greeting="hi"): return f"{greeting}, {name}"
cmd = partial(greet, "vinod", greeting="hello") # a deferred call with args captured
print(cmd()) # => hello, vinod invoke it whenever you like
A partial (or a lambda) is a command: a callable with its arguments already bound, ready to be stored in a list, put on a queue, or scheduled. Reach for the Command class only when you need more than “call it”:
| Once an action is a value you can… | Pythonic mechanism |
|---|---|
| Call it later | partial(fn, *args) / a closure |
| Store a queue of them | A list/collections.deque of callables |
| Run it on a worker | functools.partial (picklable if args are) → a job queue |
| Undo it | A small object holding do and undo callables |
| Name it for logging | Give the callable a __name__, or wrap it in a dataclass |
| Schedule it | sched, threading.Timer, or an async task taking the callable |
The first three need no class at all; only undo, naming and serialisation push you toward a Command object.
Template Method — a fixed skeleton with pluggable steps
The problem: an algorithm has a fixed shape but a few steps that vary. Template Method puts the skeleton in a base class and leaves the varying steps as methods subclasses override:
from abc import ABC, abstractmethod
class Report(ABC):
def render(self): # the template: the fixed skeleton
return self.header() + " | " + self.body() + " | " + self.footer()
def header(self): return "REPORT" # a hook with a default
@abstractmethod
def body(self): ... # a hook the subclass MUST fill
def footer(self): return "end"
class Sales(Report):
def body(self): return "sales=42"
print(Sales().render()) # => REPORT | sales=42 | end
The pythonic form keeps the skeleton in a function and takes the varying steps as callable arguments — Strategy and Template Method are the same idea once functions are first-class:
def render(body, header=lambda: "REPORT", footer=lambda: "end"):
return f"{header()} | {body()} | {footer()}"
print(render(body=lambda: "sales=42")) # => REPORT | sales=42 | end
print(render(body=lambda: "q4", footer=lambda: "confidential")) # => REPORT | q4 | confidential
No subclass, no ABC — you pass the steps that vary and default the ones that don’t. The base-class form is worth it only when the varying steps are numerous, stateful, and genuinely belong together as a type.
| Pattern | Intent | Pythonic collapse | Keep the class when |
|---|---|---|---|
| Adapter | Make interface B look like interface A | A function, if A is one method | A is several methods / must be a drop-in object |
| Decorator (GoF) | Add responsibilities, stackable, same interface | Often a @decorator or a wrapping function |
You stack object-level responsibilities at runtime |
| Command | Encapsulate an action as a value | A closure / functools.partial |
You need undo, naming, or serialisation |
| Template Method | Fixed skeleton, pluggable steps | A function taking callables (= Strategy) | Many stateful steps that form a real type |
Dependency Injection: the pattern that makes tests easy
Dependency Injection (DI) is not a GoF pattern, but it is the most important idea in this lesson, because it is what makes everything above testable. The idea is a single sentence: an object should receive its collaborators, not construct or import them.
Here is the anti-pattern — a function that reaches out to a global — and why it is untestable:
import time
def make_id_bad():
return f"ID-{int(time.time())}" # reaches for the real clock — value is non-deterministic
You cannot assert what make_id_bad() returns, because it depends on the wall clock. Your only recourse is to monkeypatch the global time.time — reaching across module boundaries to replace it, which is fragile and leaks if you forget to undo it. Inject the dependency instead:
def make_id(now): # `now` is injected
return f"ID-{now()}"
print(make_id(lambda: 1000)) # => ID-1000 fully deterministic in a test
print(make_id(lambda: 2000)) # => ID-2000
The test passes a fake now and asserts an exact value — no patching, no globals, no teardown. The same idea at the object level is constructor injection: an object takes its collaborators as __init__ arguments.
class Service:
def __init__(self, clock, store): # collaborators passed in, not imported
self.clock = clock
self.store = store
def record(self, event):
self.store.append((self.clock(), event))
log = []
svc = Service(clock=lambda: 42, store=log) # inject a fake clock and a list "store"
svc.record("login")
print(log) # => [(42, 'login')]
In tests you pass a fake clock and a plain list; in production you pass the real clock and a database. The Service code is identical in both — that is the whole payoff. Contrast the two mechanically:
| Reach for a global / singleton | Inject the dependency | |
|---|---|---|
| How the collaborator arrives | import db / Config() inside the method |
Passed to __init__ or the function |
| Test setup | Monkeypatch the global; remember to undo it | Pass a fake; nothing to undo |
| Test isolation | State leaks between tests | Each test builds its own fresh fakes |
| Swap implementations | Edit the code | Pass a different object |
| Reading the code | Hidden dependencies, discovered at runtime | Dependencies are in the signature |
| Coupling | To a concrete module/class | To an interface (a shape) |
This ties directly back to Singleton. The reason Counter() poisoned the tests earlier is that it was a hidden global; the reason Service(clock=..., store=...) does not is that its collaborators are visible and replaceable. You almost never need a singleton — you need to construct one object at your program’s entry point and inject it downward. This “construct at the top, inject down” arrangement is the entire content of the heavyweight “DI containers” from the Java world; in Python it is usually just function arguments and a bit of wiring in main(). (The testing lesson builds on exactly this — injected dependencies are what make pytest fixtures and fakes so cheap.)
The one caution: do not inject everything. Injecting a pure helper, a stdlib function, or a value that never varies just adds noise. The rule of thumb:
| Inject it (it is a seam) | Don’t inject it (it is noise) |
|---|---|
The clock (time.time, datetime.now) |
A pure function like math.sqrt |
Randomness (random.choice, uuid4) |
A constant that never varies |
| Network / HTTP clients | A private helper only this class uses |
| The database / a repository | A dataclass that is plain data |
| The filesystem | Standard-library data structures |
| The message bus / a notifier | Simple arithmetic or string formatting |
| Anything you want to fake in a test | Anything already trivial to construct in a test |
The pattern: inject the things that (a) touch the outside world — clocks, network, disk, randomness — or (b) you genuinely want to vary. Those are the seams where tests and changes happen; everything else is just a dependency you construct inline.
Iterator and Context Manager: patterns Python baked into the language
Two GoF patterns did not collapse into functions — they were promoted into syntax. You have used both for lessons without calling them patterns.
Iterator — “traverse a collection without exposing its internals” — is the iterator protocol (__iter__/__next__), and a generator is the one-line way to write one:
def countdown(n): # a generator function IS an iterator
while n > 0:
yield n
n -= 1
print(list(countdown(3))) # => [3, 2, 1]
it = iter([10, 20]) # every built-in collection hands you an iterator
print(next(it), next(it)) # => 10 20
next(it) # StopIteration the protocol's "I'm done" signal
Where Java writes a hasNext()/next() iterator class, Python writes yield and the language builds the iterator for you. for, list(), unpacking, comprehensions and in all speak this protocol — a whole GoF pattern reduced to one keyword.
Context Manager — “guaranteed setup and teardown around a block, even on error” — is the with statement (__enter__/__exit__), and contextlib.contextmanager turns a generator into one:
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f" <{name}>") # setup, before the block
try:
yield # the `with` body runs here
finally:
print(f" </{name}>") # teardown, guaranteed — even if the body raises
with tag("b"):
print(" bold text")
# => <b>
# => bold text
# => </b>
This is the pattern behind open(), threading.Lock, sqlite3 transactions and countless resource handles. It is worth naming them as patterns precisely so you stop reinventing them: if you catch yourself writing paired setup/teardown methods that callers must remember to call in order, you want a context manager; if you are exposing has_next/get_next, you want a generator. The language already has the pattern — use the keyword.
| Native pattern | GoF name | Python syntax | You almost never write |
|---|---|---|---|
| Iterate without exposing internals | Iterator | __iter__/__next__, yield |
A hasNext()/next() class |
| Guaranteed setup/teardown | (resource) | with, __enter__/__exit__ |
Manual try/finally at every call site |
| React to state change | Observer | property setter + callbacks |
A full Subject hierarchy |
| Pick behaviour by argument | Strategy | A function argument (key=) |
A Strategy class per algorithm |
The master table: pattern → intent → pythonic form → when to keep the class
This is the reference to bookmark. Read a row as: the pattern, the one-line intent, what it collapses to in Python, and the specific condition under which the full OO version still earns its keep.
| Pattern | Intent (the question it answers) | Pythonic form | Keep the classic OO version when |
|---|---|---|---|
| Strategy | Vary the algorithm without editing the caller | A function passed in (key=fn) |
The strategy carries state + several methods |
| Factory Method | Vary the created type without editing the constructor | A function returning the type | The factory holds state (a pool, credentials) |
| Abstract Factory | Create a family of related products | A registry (@register) or a module of callables |
Families are large and swapped as a unit |
| Observer | React to an event without the source knowing who | A list of callbacks / an EventBus |
Observers are stateful objects doing many things |
| Singleton | Exactly one shared instance | A module (or inject one object) | Almost never — prefer injection |
| Adapter | Make interface B look like interface A | A wrapping function (if A is one method) | A is several methods / must be a drop-in |
| Decorator (GoF) | Add stackable responsibilities, same interface | A @decorator or a wrapping function |
You stack object-level responsibilities at runtime |
| Command | Treat an action as a value (queue/undo/log) | A closure / functools.partial |
You need undo, naming, or serialisation |
| Template Method | Fixed skeleton, pluggable steps | A function taking callables | Many stateful steps that form a type |
| Iterator | Traverse without exposing internals | yield / the iterator protocol |
Basically never — the protocol is enough |
| State | Behaviour changes with an internal mode | A dict of handler functions keyed by state | States are rich objects with much behaviour |
| Chain of Responsibility | Pass a request along handlers until one takes it | A list of functions you loop over | Handlers are stateful and dynamically reordered |
| Visitor | Add operations to a type hierarchy from outside | functools.singledispatch |
You genuinely have the double-dispatch problem |
| Builder | Construct a complex object step by step | Keyword arguments / a dataclass with defaults | Construction is genuinely multi-step and validated |
| Proxy | Stand in for another object | __getattr__ forwarding / a wrapping function |
You need lazy loading, access control, or caching |
| Mediator | Centralise how objects interact | An EventBus or a coordinating function |
The interaction rules are complex and stateful |
Notice the two dominant right-hand columns: “a function” and “a module/registry/dict.” That is not a coincidence — it is Norvig’s observation in tabular form. First-class functions dissolve the behavioural patterns; the module system and dicts dissolve the creational ones; the language keyword dissolves Iterator and Context Manager. What survives as real objects are the patterns with genuine state — and the tell, every time, is state.
Because “state is the tell” does so much work, it is worth making concrete. Here is the same “carry some behaviour plus a little configuration” need, expressed at four levels of ceremony — pick the lowest one that meets your actual requirements:
| Form | Carries state? | Comparable / has repr? | Reach for it when |
|---|---|---|---|
lambda / bare def |
No (or via closure) | No | A one-off; the behaviour is all there is |
Closure (by_weight(20)) |
Yes — captured variables | No | You need configuration but nothing inspectable |
Callable object (__call__) |
Yes — on the instance | Yes, if you write the dunders | You want a good repr, or several related methods |
@dataclass(frozen=True) with __call__ |
Yes — inspectable fields | Yes, for free | The strategy must be compared, hashed, or loaded from config |
Every step down that table adds ceremony and buys inspectability. Most strategies live on the first two rows; the day one needs to appear in a config table or a test assertion is the day it earns the fourth. Do not start at the bottom.
When a pattern earns its keep (and when it is just YAGNI)
Patterns have a cost — indirection, more files, more names, a reader who must hold more in their head — and that cost is paid whether or not the flexibility is ever used. YAGNI (“You Aren’t Gonna Need It”) is the discipline of not paying until you must. The question is never “which pattern fits?” but “is the flexibility this pattern buys worth its cost against a change I can actually name?”
| Signal that a pattern earns its keep | Signal it is over-engineering (YAGNI) |
|---|---|
| There are already three-plus variants of the varying thing | There is exactly one, and “there might be more someday” |
| A new variant is added often, by different people | The set has been stable for a year |
| The variants are chosen at runtime (config, plugin, user input) | The choice is fixed at code-writing time |
| Removing the pattern would force edits across many files | The pattern spans one file you fully control |
| The seam is at a boundary (I/O, plugins, the network) | The seam is deep in pure internal logic |
| You can name the change you are protecting against | You are protecting against “flexibility” in the abstract |
| Tests get easier (a fake drops in) | Tests get harder (more setup, more mocks) |
The single most useful heuristic is the Rule of Three: do not abstract on the first case, or even the second — write it directly, duplicated if need be. Abstract on the third, when the shape of what varies is finally clear. Premature abstraction is worse than duplication, because a wrong abstraction is expensive to unpick and a duplication is cheap to delete. The patterns above are what you reach for at the third case, not before it.
And know the anti-patterns that cargo-culting produces, because they have names and you will see them in review:
| Anti-pattern | What it looks like | The fix |
|---|---|---|
| God object | Manager/Handler/Utils class that does everything |
Split by responsibility; most methods want to be functions or small classes |
| Poltergeist | A class whose only job is to call another class | Delete it; call the other class directly |
| Cargo-cult pattern | AbstractSingletonProxyFactoryBean for one concrete type |
Use a function; add the pattern when a second type appears |
| Speculative generality | Hooks, plugins and config for cases that don’t exist | Delete the unused flexibility; add it when a case is real |
| Class that is one method | A class with __init__ + do() and nothing else |
It is a function; make it a function |
| Singleton-as-global | A Config class that refuses to be built twice |
A module, or inject one instance |
If you remember one sentence from this lesson, make it this: the best pattern is usually a function, and the second-best is a plain object you inject; reach past those two only when state or a named, recurring change forces you to.
Hands-on lab
You will build a small but real checkout system that uses four patterns idiomatically — a registry factory for notification channels, Strategy as pluggable pricing functions, an Observer event bus, and dependency injection so the whole thing is trivially testable — and then write a pytest suite that is easy precisely because the dependencies are injected. Everything is pure standard library except pytest.
Setup. This lab targets Python 3.12+. Create a folder and a virtual environment (on Windows use .venv\Scripts\activate and python instead of python3):
mkdir shoplab && cd shoplab
python3 -m venv .venv && source .venv/bin/activate
python3 --version # Python 3.12.3
pip install pytest # the only third-party dependency
Step 1 — shop.py: Strategy as pluggable functions. Create shop.py and start with the pricing rules. A rule is any callable Decimal -> Decimal; no base class, no interface.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Callable
def inr(x) -> Decimal:
return Decimal(str(x))
def fmt(m: Decimal) -> str:
return f"INR {m:,.2f}"
# STRATEGY: pricing rules are just callables Decimal -> Decimal
PriceRule = Callable[[Decimal], Decimal]
def percent_off(pct: int) -> PriceRule:
def rule(subtotal: Decimal) -> Decimal:
return subtotal - subtotal * Decimal(pct) / Decimal(100)
return rule
def flat_fee(fee) -> PriceRule:
fee = inr(fee)
def rule(subtotal: Decimal) -> Decimal:
return subtotal + fee
return rule
def apply_rules(subtotal: Decimal, rules) -> Decimal:
for rule in rules: # order is EXPLICIT — you wrote it in a tuple
subtotal = rule(subtotal)
return subtotal
What just happened: percent_off(10) and flat_fee(50) are closures — configured strategies — and apply_rules composes them in the order you pass, with no if discount_type == ... anywhere. (This mirrors the composition-over-inheritance result from the domain models lesson: the tuple makes the order of operations explicit instead of leaving it to an MRO.)
Step 2 — a decorator-built registry factory for channels. Append this. A channel is registered by a decorator, so adding one never edits a central list.
# FACTORY: a decorator-built registry of notification channels
_CHANNELS: dict[str, type] = {}
def channel(name: str):
def deco(cls):
if name in _CHANNELS:
raise ValueError(f"channel {name!r} already registered")
_CHANNELS[name] = cls
return cls
return deco
def make_channel(name: str, **kwargs):
try:
return _CHANNELS[name](**kwargs)
except KeyError:
raise ValueError(f"unknown channel {name!r}; "
f"have {sorted(_CHANNELS)}") from None
@channel("email")
@dataclass
class Email:
outbox: list = field(default_factory=list) # NOT `outbox: list = []`
def send(self, msg: str) -> None:
self.outbox.append(("email", msg))
@channel("sms")
@dataclass
class Sms:
outbox: list = field(default_factory=list)
def send(self, msg: str) -> None:
self.outbox.append(("sms", msg))
What just happened: @channel("email") records the class in _CHANNELS at import time and returns it unchanged. make_channel("email") builds one by name. Note field(default_factory=list) — a mutable default in a registered class would share one outbox across every instance, the exact bug the troubleshooting table below catches.
Step 3 — an Observer event bus. Append the bus. Publishers and subscribers never reference each other.
# OBSERVER: a tiny event bus (pub/sub)
class EventBus:
def __init__(self) -> None:
self._subs: dict[str, list[Callable]] = defaultdict(list)
def on(self, event: str, handler: Callable):
self._subs[event].append(handler)
return handler # returnable -> also usable as a decorator
def off(self, event: str, handler: Callable) -> None:
self._subs[event].remove(handler) # unsubscribe -> no memory leak
def emit(self, event: str, **data) -> None:
for handler in list(self._subs[event]): # copy: a handler may unsubscribe
handler(**data)
What just happened: defaultdict(list) means on() never checks whether an event exists; off() exists so subscribers can be released (the leak fix); and emit iterates list(...), a copy, so a handler unsubscribing mid-emit cannot corrupt the loop.
Step 4 — the Checkout service with dependency injection. Append it. The service receives its collaborators; it constructs none of them.
# DEPENDENCY INJECTION: the service is handed its collaborators
@dataclass
class Checkout:
bus: EventBus # injected -> a fake bus in tests
clock: Callable[[], int] # injected -> deterministic in tests
rules: tuple[PriceRule, ...] = () # injected Strategy list
def place(self, order_id: str, subtotal) -> Decimal:
total = apply_rules(inr(subtotal), self.rules)
self.bus.emit("order.placed", order_id=order_id,
total=total, at=self.clock())
return total
What just happened: Checkout knows nothing about email, SMS, the wall clock, or which discounts apply. It gets a bus, a clock and a rules tuple. That is the seam that makes Step 6’s test a two-liner.
Step 5 — demo.py: wire it together and run it. Create demo.py alongside shop.py:
from shop import (Checkout, EventBus, make_channel, percent_off, flat_fee, fmt)
bus = EventBus()
email = make_channel("email")
sms = make_channel("sms")
# Observer: channels subscribe to the event; Checkout knows none of them
bus.on("order.placed",
lambda order_id, total, **_: email.send(f"{order_id}: {fmt(total)}"))
bus.on("order.placed",
lambda order_id, at, **_: sms.send(f"{order_id} placed at t={at}"))
# DI: hand the service a fixed clock and the rule Strategy list
checkout = Checkout(bus=bus, clock=lambda: 1000,
rules=(percent_off(10), flat_fee(50)))
total = checkout.place("ORD-1", 484)
print("total :", fmt(total)) # (484 * 0.9) + 50
print("email outbox:", email.outbox)
print("sms outbox :", sms.outbox)
Run it:
python3 demo.py
total : INR 485.60
email outbox: [('email', 'ORD-1: INR 485.60')]
sms outbox : [('sms', 'ORD-1 placed at t=1000')]
What just happened: one checkout.place(...) call priced the order through the strategy tuple (484 × 0.9 + 50 = 485.60), emitted one event, and two independently-registered channels reacted — none of which Checkout imports or names. Adding a Slack channel is a new @channel("slack") class plus one bus.on(...); nothing in Checkout changes.
Step 6 — test_shop.py: the payoff. Create the test file. Watch how little each test needs, because every dependency is injected:
from decimal import Decimal
from shop import Checkout, EventBus, percent_off, flat_fee, make_channel
def test_place_applies_rules_in_order():
checkout = Checkout(bus=EventBus(), clock=lambda: 0,
rules=(percent_off(10), flat_fee(50)))
# (484 * 0.9) + 50 == 485.60, NOT (484 + 50) * 0.9
assert checkout.place("ORD-1", 484) == Decimal("485.60")
def test_place_emits_event_with_injected_clock():
seen = []
bus = EventBus()
bus.on("order.placed", lambda **data: seen.append(data))
# the injected clock makes the timestamp deterministic -- no monkeypatch
checkout = Checkout(bus=bus, clock=lambda: 42, rules=(percent_off(10),))
total = checkout.place("ORD-9", 200)
assert total == Decimal("180")
assert seen == [{"order_id": "ORD-9", "total": Decimal("180"), "at": 42}]
def test_channels_are_pluggable_from_the_registry():
email = make_channel("email")
email.send("hi")
assert email.outbox == [("email", "hi")]
def test_unknown_channel_is_a_clear_error():
import pytest
with pytest.raises(ValueError, match="unknown channel 'fax'"):
make_channel("fax")
Run the suite:
python3 -m pytest -v
test_shop.py::test_place_applies_rules_in_order PASSED [ 25%]
test_shop.py::test_place_emits_event_with_injected_clock PASSED [ 50%]
test_shop.py::test_channels_are_pluggable_from_the_registry PASSED [ 75%]
test_shop.py::test_unknown_channel_is_a_clear_error PASSED [100%]
============================== 4 passed in 0.00s ===============================
What just happened — and this is the entire point of the lab: test_place_emits_event_with_injected_clock asserts an exact timestamp of 42 with no monkeypatching, no freezegun, no global reset, because the clock is a constructor argument — pass lambda: 42 and time stands still. A fake bus is a real EventBus with a list-appending subscriber. There is no singleton to clear between tests, so the tests do not interfere and can run in any order. Every one of those properties is a direct consequence of the four patterns being implemented pythonically: functions for strategies, a registry for factories, a bus for observers, and injection for collaborators. Had Checkout reached for time.time() and a global Config() singleton, each of these tests would have needed teardown and patching — and one of them would be flaky.
⚠️ One caution for your own registries: _CHANNELS is module-global state. In a long-lived test suite that registers channels dynamically, clear or snapshot it in a fixture so one test’s registrations do not leak into another — the same global-state discipline the Singleton section warned about, now applied to your own registry.
Common mistakes and troubleshooting
| Symptom / traceback | Cause | Fix |
|---|---|---|
A one-method class with __init__ + do() and nothing else |
A function wearing a class costume (Strategy/Command over-built) | Make it a function or a closure; pass it in |
AssertionError in a test that passes when run alone |
A Singleton’s state leaked from a previous test | Inject the object (fresh per test) instead of a singleton; if stuck, reset the singleton in teardown |
__init__ runs twice and wipes state on a __new__ singleton |
__new__ returns the cache but Python still calls __init__ every time |
Guard __init__ with an _initialised flag, or use a module, or inject |
ValueError: mutable default <class 'list'> on a registered dataclass |
outbox: list = [] shares one list across all instances |
field(default_factory=list) |
RuntimeError: dictionary changed size during iteration (or a skipped observer) |
A handler subscribed/unsubscribed while emit looped the live list |
Iterate a copy: for h in list(self._subs[event]) |
| Objects never garbage-collected; memory grows | A subject holds strong refs to subscribers that never unsubscribe | Provide and call off(); or hold weakref.ref / use weakref.WeakSet |
TypeError: unknown notifier 'x' deep in a call, no context |
A factory KeyError re-raised without a clear message |
raise ValueError(f"unknown {x!r}; have {sorted(reg)}") from None |
A Manager/Handler class of 1,000+ lines touched by every PR |
God-object anti-pattern from cargo-culted “manager” thinking | Split by responsibility; extract functions and small injected collaborators |
| Changing one class forces edits in five unrelated files | Tight coupling from import-ing concrete classes instead of injecting |
Depend on a passed-in interface (a callable/Protocol), not a concrete import |
An adapter exposes both .read() and the adaptee’s .fetch_celsius_times_ten() |
The adapter leaks the adaptee’s interface | Forward only the target interface; keep the adaptee private (self._legacy) |
| A five-level inheritance tree to combine features | Inheritance where composition was needed | Compose small strategy/rule objects in a list (see domain-models lesson) |
TypeError: 'X' object is not callable when using an object as a strategy |
You passed a class, not an instance/function, or forgot __call__ |
Pass fn, Cls(), or give the class __call__ |
| A pattern with exactly one implementation and a “TODO: more later” | Speculative generality / premature abstraction | Delete the abstraction; inline it; re-add at the third real case |
Three of these cost the most time and deserve prose.
1. The Singleton that makes tests flaky. This is the deepest one, because the failure appears far from its cause. You write a Config singleton, everything works, and months later a colleague adds a test that mutates config; now an unrelated test that runs after it fails intermittently, depending on test order. The debugging is miserable because nothing in either test mentions the other. The root cause is that a singleton is global mutable state, and tests must be independent. The permanent fix is not “reset the singleton in teardown” (that is a patch on a patch) — it is to inject the dependency so each test constructs its own. If you truly cannot (legacy code), at minimum expose a reset and call it in a fixture, and treat every such reset as a design debt to repay.
2. The observer memory leak. A subject keeps a list of subscribers, and a Python list holds strong references — so a subscriber the subject never releases can never be garbage-collected, and neither can anything its closure captured. In a long-running server (a GUI, a web app, a daemon) this is a slow leak that surveillance tools eventually flag as “memory grows over time.” You can watch it happen and watch the fix work:
import weakref, gc
class Bus:
def __init__(self): self.handlers = []
def on(self, h): self.handlers.append(h)
class BigObject: # a stand-in for anything expensive
pass
def subscribe(bus):
big = BigObject() # local -> the lambda closes over a real cell
bus.on(lambda ev: big) # ONLY the bus holds the handler (and thus big)
return weakref.ref(big)
bus = Bus()
ref = subscribe(bus)
gc.collect()
print("leaked (bus still holds it):", ref() is not None) # => True still alive
bus.handlers.clear() # unsubscribe -> break the last reference
gc.collect()
print("after unsubscribe:", ref() is not None) # => False freed
The object stays alive purely because the bus’s closure references it, and dies the moment the bus lets go. The fixes, in order of preference: give the bus an off() and call it when a subscriber’s life ends; or store subscribers in a weakref.WeakSet / hold weakref.refs so the subject does not keep them alive; or, for bound methods, use weakref.WeakMethod. The plain list is fine for short-lived buses and a footgun for long-lived ones.
3. Over-engineering: the class that should have been a function. The most common pattern mistake in Python is not a wrong pattern — it is a present pattern that should have been absent. A class DiscountStrategy with an __init__ storing one number and an apply() doing one calculation is a function with extra steps; a NotificationManagerFactoryProvider for one concrete notifier is four words of ceremony around EmailNotifier(). The cost is real: every layer of indirection is a jump the next reader must make, a file they must open, a name they must learn, for flexibility that is never exercised. The discipline is the Rule of Three and the “can I name the change?” test. When in doubt, write the function; a function is trivially promoted to a class the day it grows state, and a class is painful to demote to a function the day you realise it never needed to be one.
Cheat-sheet
| Pattern / tool | Pythonic form |
|---|---|
| Strategy | def do(...); pass it: Ctx(strategy=do), call self.strategy(x) |
| Configured strategy | A closure: by_weight(20) returns the configured function |
| Sort strategy | sorted(xs, key=fn) / key=attrgetter("age") |
| Factory (few types) | def make(kind): ... raise ValueError(...) |
| Factory (data-driven) | REGISTRY = {"email": Email}; REGISTRY[k]() |
| Factory (extensible) | @register("name") decorator writing into a dict |
| Factory (by type) | @functools.singledispatch |
| Factory (closed set) | Enum |
| Observer (simple) | self._cbs.append(fn); loop and call in emit |
| Observer (as decorator) | subscribe returns fn; use @subject.subscribe |
| Observer (many events) | EventBus with defaultdict(list), on/off/emit |
| Observer safety | emit: for h in list(self._subs[e]) (iterate a copy) |
| Observer no-leak | provide off(); or weakref.WeakSet / WeakMethod |
| Singleton | a module (import settings) — interpreter-cached |
| Singleton (avoid) | __new__ guard, metaclass, Borg — all leak in tests |
| Adapter | wrap the adaptee (self._x), expose only the target method |
| Decorator (GoF) | wrap an object, same interface, stackable at runtime |
Decorator (@) |
@functools.wraps(fn) inside def wrapper(*a, **k) |
| Command | a closure / functools.partial(fn, *args) |
| Command (with undo) | a small object holding do/undo callables |
| Template Method | def render(step=default_fn) — pass the varying steps |
| Dependency Injection | pass collaborators to __init__ / the function |
| DI test win | Service(clock=lambda: 42, store=[]) — deterministic, no patch |
| Iterator | def gen(): yield ... — a generator is an iterator |
| Context Manager | @contextlib.contextmanager + try/yield/finally |
| State | HANDLERS = {"open": open_fn}; HANDLERS[state](...) |
| Chain of Responsibility | a list of functions you loop until one returns |
| Visitor | @functools.singledispatch |
| Proxy | __getattr__ forwarding to a wrapped object |
| Rule of Three | Don’t abstract at 1 or 2 variants; abstract at 3 |
| The tell for “keep the class” | The thing has state, not just one behaviour |
Interview and exam questions
Q: Why do so many Gang-of-Four patterns “disappear” in Python?
A: Because most of them are workarounds for two things Python has natively: first-class functions and dynamic (duck) typing. Strategy, Command, Template Method and Observer all exist to move behaviour around, and in Python behaviour is a function you can pass, return and store — so they collapse to “pass a function.” Factory patterns exist to create objects by name at runtime, which Python does with a class-as-value in a dict or a decorator registry. Singleton is handled by the module system. What survives as real OO patterns are the ones with genuine state — the tell, every time, is whether the thing has state and several behaviours, or is one behaviour dressed as an object.
Q: Implement Strategy in Python, then show the pythonic version.
A: The OO version is an ABC with a method, one class per algorithm, and a context that holds one and delegates: class Order: def cost(self): return self.shipping.cost(self.w). The pythonic version drops the ABC and the classes and passes a function: Order(shipping=by_weight(20)), calling self.shipping(self.w). by_weight(20) is a closure carrying its configuration. Python’s own sorted(data, key=fn) is Strategy — the standard library never defines a SortStrategy class. Keep the class form only when the strategy has state plus several methods (a retry policy), or must be compared/serialised.
Q: You need a factory that lets people add new types without editing a central file. What do you build?
A: A decorator-based registry: a module-level dict, and a register(name) decorator that records each class in it and returns the class unchanged. New types live in their own files with @register("name") on top; a create(name) function looks the class up and instantiates it, raising a clear ValueError listing the known names on a miss. This is exactly how Flask routes, Click commands and pytest plugins register — open for extension, closed for modification, with zero factory classes. For a small fixed set, a plain function or a dict literal is enough; reach for the decorator registry when the set is open and extended by others.
Q: What is the pythonic way to make a singleton, and why avoid the class-based versions?
A: Use a module — Python imports a module once and caches it in sys.modules, so module-level state is a singleton the interpreter enforces, with no boilerplate. Avoid __new__/metaclass/Borg singletons mainly because they are global mutable state that breaks tests: state leaks between tests, so a test that passes alone fails when run after another, and you end up writing fragile per-test reset code. The __new__ form has an extra trap — Python calls __init__ on the cached instance every time you “construct” it, silently re-running initialisation and wiping state. The real fix for “I need one shared thing” is usually dependency injection: build one at the program’s entry point and pass it down, so tests can pass a fresh one.
Q: What is the difference between the Decorator pattern and a Python @decorator?
A: They rhyme but are different. The GoF Decorator pattern wraps an object to add responsibilities while presenting the same interface, and wrappers stack at runtime — SugarDecorator(MilkDecorator(Coffee())). A Python @decorator wraps a function or class at definition time to transform it — @timed def add(...). Both “wrap X, preserve X’s interface,” but they operate on different things at different times. The @ syntax is a language feature for the “wrap a callable” case; the pattern is a runtime object-composition technique. Confusing them is common; naming the distinction (object vs callable, runtime vs definition time) is the answer.
Q: Why does injecting a dependency make code more testable than reaching for a global?
A: Because a test can pass a fake in one line, with nothing to undo. A function that calls time.time() internally can only be tested by monkeypatching the global time.time — reaching across module boundaries, fragile, and it leaks if you forget to restore it. The same function written as make_id(now) is tested with make_id(lambda: 1000) and an exact assertion. At the object level, constructor injection (Service(clock=..., store=...)) means production passes a real clock and DB while tests pass a fake clock and a list — identical code, swappable collaborators. Injection also makes dependencies visible in the signature instead of hidden inside the body, and it kills the test-isolation problems that globals and singletons cause.
Q: When should you not use a design pattern? A: When you cannot name the change you are protecting against. Patterns cost indirection, files, and reader effort, paid whether or not the flexibility is used. Apply the Rule of Three: write the first and second case directly (duplicated if needed), and abstract only at the third, when the varying shape is clear — because a wrong abstraction is far more expensive than duplication. Concretely: don’t build a Strategy for one algorithm, a Factory for one type, or an Observer for one hard-wired reaction. A present-but-unneeded pattern (a one-method “strategy” class, a God-object “manager,” a singleton config) is the most common pattern mistake in Python, more common than choosing the wrong pattern.
Q: A subject notifies observers, and in a long-running server memory keeps growing. Why, and how do you fix it?
A: The subject stores observers in a list, and lists hold strong references, so any observer the subject never releases can never be garbage-collected — along with everything its closure captured. In a daemon or GUI that subscribes objects over time without unsubscribing, that is a slow leak. Fixes, best first: give the subject an off()/unsubscribe and call it when an observer’s life ends; store observers in a weakref.WeakSet or as weakref.refs so the subject does not keep them alive; for bound methods use weakref.WeakMethod. Also, always iterate a copy in notify/emit, because an observer that unsubscribes itself mid-notification would otherwise mutate the list being looped.
Q (coding): Turn this if/elif construction ladder into an extensible registry.
def make_exporter(fmt):
if fmt == "csv": return CsvExporter()
elif fmt == "json": return JsonExporter()
elif fmt == "xml": return XmlExporter()
raise ValueError(fmt)
A: Replace the ladder with a decorator registry so new exporters self-register:
_EXPORTERS = {}
def exporter(name):
def deco(cls):
_EXPORTERS[name] = cls
return cls
return deco
@exporter("csv")
class CsvExporter: ...
@exporter("json")
class JsonExporter: ...
def make_exporter(fmt):
try:
return _EXPORTERS[fmt]()
except KeyError:
raise ValueError(f"unknown format {fmt!r}; have {sorted(_EXPORTERS)}") from None
Now adding an XmlExporter is a new file with @exporter("xml") on top — make_exporter never changes. If the set were small and closed, a dict literal {"csv": CsvExporter, ...} would be enough; the decorator earns its keep when third parties extend the set.
Q (coding): Make this function testable without monkeypatching.
import random
def pick_winner(names):
return random.choice(names)
A: Inject the source of randomness:
def pick_winner(names, choose=random.choice):
return choose(names)
Production calls pick_winner(names) and gets real randomness; a test calls pick_winner(["a", "b"], choose=lambda xs: xs[0]) and asserts "a" deterministically — no random.seed, no monkeypatch, no global reach. Randomness, clocks, network and disk are exactly the dependencies worth injecting, because they are what make code non-deterministic and slow to test.
Q: Which GoF patterns did Python promote into language syntax, and what are the tells that you want them?
A: Iterator became the iterator protocol and yield — if you catch yourself writing has_next()/get_next(), you want a generator. Context Manager (a resource-management pattern) became with/__enter__/__exit__ — if you are writing paired setup/teardown methods callers must remember to call in order, you want a context manager (or @contextlib.contextmanager). Observer partially collapsed into property setters plus callbacks for the “react to state change” case. Strategy collapsed into passing a function (key=). The general lesson: when a pattern becomes syntax, stop hand-rolling it — use the keyword.
Key takeaways
- Most Gang-of-Four patterns are workarounds for missing language features, and Python has the features. First-class functions dissolve the behavioural patterns (Strategy, Command, Template Method, Observer → pass a function); the module system and dicts dissolve the creational ones (Factory → a dict or a decorator registry; Singleton → a module); and
yield/withpromote Iterator and Context Manager into syntax. The patterns that survive as real objects are the ones with genuine state — that is the tell, every time. - Strategy is a function you pass in. The OO version — an ABC, a class per algorithm, a context that delegates — collapses to
ctx.strategy = fn, exactly whatsorted(xs, key=fn)does. Keep the class only when the strategy carries state plus several methods, or must be compared/serialised (a frozen-dataclass strategy). - Factory is a dict or a decorator registry. For a few fixed types, a function; for a growing set,
dictdispatch on the class-as-value; for an open, third-party-extensible set, a@register("name")decorator — the pattern behind Flask, Click and pytest.singledispatchcovers “by argument type,”Enumcovers a closed vocabulary. Keep a factory class only when the factory itself holds state. - Singleton is a module; the class-based forms break tests. Module state is an interpreter-enforced singleton with no boilerplate.
__new__/metaclass/Borg singletons are global mutable state that leaks between tests (and__new__re-runs__init__, silently wiping state). What you usually need is not “one instance” but “don’t construct twelve” — construct one at the entry point and inject it. - Dependency Injection is the pattern that makes everything testable. Objects should receive collaborators, not import or construct them.
make_id(now)andService(clock=..., store=...)let tests pass fakes in one line with nothing to undo — no monkeypatching, no global reset, no flakiness. Inject the things that touch the outside world (clocks, network, disk, randomness) or that you genuinely want to vary; don’t inject pure helpers that never change. - Adapter, Command, Template Method and the Decorator pattern each collapse along their own line. Adapter → a wrapping function when the target is one method (keep the class for multi-method interfaces; never leak the adaptee). Command → a closure or
functools.partial(keep the object only for undo/naming/serialisation). Template Method → a function taking callable steps (= Strategy). And the GoF Decorator (wrap an object, stackable at runtime) is a cousin of, not the same as, Python’s@decorator(transform a callable at definition time). - Patterns are a vocabulary, not a checklist — obey the Rule of Three. Don’t abstract on the first or second case; write it directly, duplicated if needed, and abstract on the third, when the varying shape is clear, because a wrong abstraction costs more than duplication. The most common pattern mistake in Python is a present pattern that should be absent: a one-method “strategy” class, a God-object “manager,” a
Factoryfor one type, a Singleton config. If you cannot name the change you are protecting against, you are decorating, not designing. - The best pattern is usually a function; the second-best is a plain object you inject. Reach past those two only when state, or a named and recurring change, forces you to — and remember that a function is trivially promoted to a class the day it grows state, while a class is painful to demote the day you realise it never needed to be one.
This lesson is the judgement half of the professional-Python arc: the OOP series taught you the machinery of classes, domain models taught you to compose small objects instead of inheriting, functional Python taught you that behaviour is a value you can pass around, and testing taught you why injected dependencies are worth the wiring. Design patterns are where those four meet: knowing them is knowing which to not reach for, and trusting that in Python the answer is very often a function, a dict, or a module.