Chapter 7: Pattern Implementation in Python
Python offers a rich toolbox of language features that reshape how classical patterns appear in real projects. Some patterns fade because Python already solves the problem; some map directly with almost no change; others need adaptation so they feel natural in a language that prizes readability and minimal boilerplate.
Pythonic Idioms That Replace Patterns
Several patterns shrink or disappear entirely thanks to modules, first class functions, dynamic attributes, and the expressive data model. Python’s built ins often act like pattern accelerators because the language already expects code to be stitched together from small cooperators.
Singleton
Instead of building a formal Singleton, Python tends to use modules. Importing a module returns the same instance each time, and its top level names behave like shared state. If you really need a controlled object, factories or explicit wiring usually work better than strict singletons.
# settings.py
config = {"debug": True}
# anywhere else
import settings
print(settings.config["debug"])
Strategy
Strategy often becomes a collection of plain functions or callables. Passing a function into another function reads cleanly and avoids setting up tiny classes that only wrap one calculation.
def sum_strategy(data): return sum(data)
def max_strategy(data): return max(data)
def run(data, strat): return strat(data)
run([1, 2, 3], sum_strategy)
Command
Python’s first class functions also act as stand alone commands. You can store them, queue them, and call them later. Closures capture state, which means you do not need objects with execute() unless you prefer that shape.
def insert(doc, s):
def cmd():
doc.append(s)
return cmd
doc = []
cmd = insert(doc, "Hello")
cmd()
Patterns That Translate Cleanly
Some patterns map directly because Python’s class model supports encapsulation, delegation, and polymorphism with little ceremony. These patterns keep their usual structure but feel lighter because Python handles details such as initialization, mixins, and method passing gracefully.
Observer
Observer maps well because Python makes callbacks effortless. Storing callable subscribers and invoking them inside notify() is a natural fit.
class Subject:
def __init__(self): self.subs = []
def subscribe(self, fn): self.subs.append(fn)
def set(self, v):
self.value = v
for fn in self.subs: fn(v)
s = Subject()
s.subscribe(lambda v: print("update", v))
s.set(10)
State
State fits Python’s dynamic dispatch well. Each state class defines behavior, and the context swaps the active instance at runtime.
class StateA:
def run(self, ctx): print("A"); ctx.state = StateB()
class StateB:
def run(self, ctx): print("B"); ctx.state = StateA()
class Machine:
def __init__(self): self.state = StateA()
def step(self): self.state.run(self)
m = Machine()
m.step(); m.step()
Decorator
Decorator aligns with Python’s function decorators. The idea of wrapping behavior with another callable is baked into the syntax.
def log(fn):
def wrapper(*a, **k):
print("calling", fn.__name__)
return fn(*a, **k)
return wrapper
@log
def greet(name): print("hi", name)
greet("Ada")
Patterns That Need Adaptation
Some patterns require adjustment because Python’s dynamic nature shifts their shape. Instead of porting them verbatim, you reshape them so they feel idiomatic and avoid unnecessary scaffolding.
Abstract Factory
Abstract Factory often collapses into simple factory functions or data driven builders. Because classes are first class objects, returning a class from a function is straightforward.
def widget_factory(theme):
if theme == "dark": return DarkWidget
return LightWidget
class DarkWidget: pass
class LightWidget: pass
Widget = widget_factory("dark")
w = Widget()
Visitor
Visitor needs reshaping when Python’s dynamic type handling already solves most dispatch needs. Pattern matching using match can simplify cases where you would normally write a full visitor hierarchy.
def visit(node):
match node:
case {"type": "file", "size": s}: return s
case {"type": "folder", "items": items}:
return sum(visit(i) for i in items)
case _: raise ValueError("unknown node")
match {key: …} stays readable.
Mediator
Mediator becomes lighter because Python encourages direct callbacks, small helpers, and module level coordination. Instead of a central coordinator class, you often use event functions, registries, or async channels.
class Chat:
def __init__(self): self.users = []
def join(self, user): self.users.append(user)
def send(self, sender, msg):
for u in self.users:
if u is not sender:
u.receive(msg)
class User:
def __init__(self, name, chat): self.name, self.chat = name, chat; chat.join(self)
def send(self, msg): self.chat.send(self, msg)
def receive(self, msg): print(self.name, "<", msg)
Patterns feel at home in Python when they remain small and intentional. Translate the spirit, not the scaffolding, and let Python’s features carry the weight.