<< Home

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"])
💡 Keep module state simple so that import cycles remain easy to avoid and testing stays predictable.

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)
⚠️ Hold weak references in long lived subjects when subscribers need to be collected without manual cleanup.

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")
💡 When modeling trees, make node shapes predictable so 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.