Behavioral Design Patterns (GoF)
The eleven GoF behavioral patterns address how objects communicate and how responsibilities are distributed. They’re about algorithms, control flow, and the assignment of behavior.
Reference: refactoring.guru / Design Patterns — illustrated explanations of every GoF pattern; cross-reference each pattern below against the canonical descriptions there.
The eleven behavioral patterns:
For creational see 05_creational_patterns.md; for structural 06_structural_patterns.md.
Chain of Responsibility
Intent: Avoid coupling sender to receiver by giving multiple objects a chance to handle a request. Chain receivers; pass the request along until one handles it. Reference: https://refactoring.guru/design-patterns/chain-of-responsibility
Problem: A request might be handled by one of several objects; you don’t want the sender to know which. Each handler may decide to handle it, ignore it, or pass it to the next.
from abc import ABC, abstractmethod
from typing import Optional
class Handler(ABC):
def __init__(self):
self._next: Optional[Handler] = None
def set_next(self, handler: "Handler") -> "Handler":
self._next = handler
return handler # for chaining
def handle(self, request):
if self._next:
return self._next.handle(request)
return None
class AuthHandler(Handler):
def handle(self, request):
if not request.get("auth"):
return "401 Unauthorized"
return super().handle(request)
class RateLimitHandler(Handler):
def handle(self, request):
if request.get("requests_per_minute", 0) > 100:
return "429 Too Many Requests"
return super().handle(request)
class CacheHandler(Handler):
def handle(self, request):
if cached := request.get("_cached"):
return f"Cache hit: {cached}"
return super().handle(request)
class HandlerEnd(Handler):
def handle(self, request):
return "200 OK — handled by application"
# Build the chain
chain = AuthHandler()
chain.set_next(RateLimitHandler()).set_next(CacheHandler()).set_next(HandlerEnd())
print(chain.handle({"auth": "token", "requests_per_minute": 50})) # "200 OK"
print(chain.handle({"requests_per_minute": 50})) # "401 Unauthorized"
Real-world: HTTP middleware stacks (Django, Flask, Express), event bubbling in UIs, logging filters.
When to use: variable / configurable processing pipelines; multiple handlers eligible; loose coupling between sender and handler.
Pitfalls: chain can be incomplete (no handler matches); ordering matters; debugging traversal through long chains.
Command
Intent: Encapsulate a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undoable operations. Reference: https://refactoring.guru/design-patterns/command
Problem: You want to decouple the invoker from the receiver. The invoker says “do this command”; doesn’t know how.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self): ...
@abstractmethod
def undo(self): ...
class Light:
def __init__(self):
self.on = False
def turn_on(self): self.on = True
def turn_off(self): self.on = False
class TurnOnCommand(Command):
def __init__(self, light): self.light = light
def execute(self): self.light.turn_on()
def undo(self): self.light.turn_off()
class TurnOffCommand(Command):
def __init__(self, light): self.light = light
def execute(self): self.light.turn_off()
def undo(self): self.light.turn_on()
class RemoteControl:
def __init__(self):
self.history: list[Command] = []
def press(self, command: Command):
command.execute()
self.history.append(command)
def undo_last(self):
if self.history:
self.history.pop().undo()
light = Light()
remote = RemoteControl()
remote.press(TurnOnCommand(light)) # light on
remote.press(TurnOffCommand(light)) # light off
remote.undo_last() # light on again
When to use: undo/redo, command queues, macro recording, task scheduling, GUI buttons mapped to actions.
Pitfalls: too many command classes for trivial operations; pure ceremony for one-liners. Pythonic alternative: a callable (function or lambda) often suffices.
Interpreter
Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language. Reference: (Less commonly applied; refactoring.guru notes its decline in favor of parser generators.)
Problem: You need to evaluate expressions in a domain-specific language (math expressions, search queries, filter rules).
from abc import ABC, abstractmethod
class Expression(ABC):
@abstractmethod
def interpret(self, context: dict) -> int: ...
class Number(Expression):
def __init__(self, value): self.value = value
def interpret(self, context): return self.value
class Variable(Expression):
def __init__(self, name): self.name = name
def interpret(self, context): return context[self.name]
class Add(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self, context):
return self.left.interpret(context) + self.right.interpret(context)
class Multiply(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self, context):
return self.left.interpret(context) * self.right.interpret(context)
# Build AST: (x + 5) * 3
expr = Multiply(Add(Variable("x"), Number(5)), Number(3))
print(expr.interpret({"x": 10})) # (10 + 5) * 3 = 45
When to use: small, stable DSLs (filter expressions, simple query languages).
Real-world: Django Q-objects, SQLAlchemy expression language, math expression evaluators.
Pitfalls: complex grammars are better handled by parser generators (PLY, lark) or libraries; rolling your own interpreter doesn’t scale.
Iterator
Intent: Provide a way to access elements of a collection sequentially without exposing its underlying representation. Reference: https://refactoring.guru/design-patterns/iterator
Problem: Different collections (lists, trees, graphs) have different traversal logic. Clients shouldn’t depend on a collection’s internal structure.
In Python, iterators are first-class. The pattern is built into the language via __iter__ and __next__.
class WordCollection:
def __init__(self):
self._words: list[str] = []
def add(self, word):
self._words.append(word)
def __iter__(self):
return WordIterator(self)
class WordIterator:
def __init__(self, collection):
self._collection = collection
self._index = 0
def __iter__(self):
return self
def __next__(self):
if self._index >= len(self._collection._words):
raise StopIteration
word = self._collection._words[self._index]
self._index += 1
return word
words = WordCollection()
for w in ["apple", "banana", "cherry"]:
words.add(w)
for w in words:
print(w)
Pythonic shortcut — generators ARE iterators:
class WordCollection:
def __init__(self):
self._words = []
def add(self, word):
self._words.append(word)
def __iter__(self):
yield from self._words
When to use: custom traversal (in-order tree walk, breadth-first graph), exposing iteration without exposing internals.
Real-world: every Python for loop, itertools, ORM queryset iteration, file line-by-line reading.
Pitfalls: forgetting StopIteration, stateful iterators that can’t be re-iterated.
See ../02_python_core/08_class_iterator_generator.md and ../02_python_core/10_iterator_vs_generator.md.
Mediator
Intent: Define an object that encapsulates how a set of objects interact. Promotes loose coupling by keeping objects from referring to each other explicitly. Reference: https://refactoring.guru/design-patterns/mediator
Problem: Many objects need to talk to each other. Direct references between all pairs produce a tangle (N×N coupling). A mediator centralizes the routing.
from abc import ABC, abstractmethod
class Mediator(ABC):
@abstractmethod
def notify(self, sender, event): ...
class ChatRoom(Mediator):
def __init__(self):
self.users: list[User] = []
def register(self, user):
self.users.append(user)
user.mediator = self
def notify(self, sender, event):
for user in self.users:
if user is not sender:
user.receive(sender.name, event)
class User:
def __init__(self, name):
self.name = name
self.mediator: Mediator | None = None
def send(self, message):
print(f"{self.name} sends: {message}")
self.mediator.notify(self, message)
def receive(self, sender_name, message):
print(f"{self.name} receives from {sender_name}: {message}")
room = ChatRoom()
alice = User("Alice"); room.register(alice)
bob = User("Bob"); room.register(bob)
carol = User("Carol"); room.register(carol)
alice.send("Hi everyone!")
Users don’t reference each other. They reference the mediator.
When to use: many-to-many object interactions; UI dialogs where buttons / text fields / dropdowns affect each other; air traffic control style systems.
Pitfalls: the mediator can become a god object holding ALL the routing logic. Split mediators if it grows too large.
Real-world: message brokers (Kafka, RabbitMQ — distributed mediators), event buses, MVC controllers acting as mediators between models and views.
Memento
Intent: Without violating encapsulation, capture and externalize an object’s internal state so the object can be restored to this state later. Reference: https://refactoring.guru/design-patterns/memento
Problem: You want undo / snapshots. Exposing the object’s internal state to enable saving breaks encapsulation.
from copy import deepcopy
class EditorMemento:
def __init__(self, content: str):
self._content = content # private to the editor
def get_content(self):
return self._content
class Editor:
def __init__(self):
self.content = ""
def write(self, text):
self.content += text
def save(self) -> EditorMemento:
return EditorMemento(self.content)
def restore(self, memento: EditorMemento):
self.content = memento.get_content()
class History:
def __init__(self):
self.stack: list[EditorMemento] = []
def push(self, memento):
self.stack.append(memento)
def pop(self):
return self.stack.pop()
editor = Editor()
history = History()
editor.write("Hello")
history.push(editor.save())
editor.write(" world")
history.push(editor.save())
editor.write("!")
print(editor.content) # "Hello world!"
editor.restore(history.pop()) # back to "Hello world"
print(editor.content) # "Hello world"
When to use: undo/redo, snapshots, transactional changes.
Pitfalls: large state → large mementos → memory growth. Use diff-based memento for large objects, or limit history size.
Real-world: text editor undo, database savepoints, Git commit history (conceptually).
Observer
Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Reference: https://refactoring.guru/design-patterns/observer
Problem: Multiple parts of a system need to react to events in another part. Hard-coding the dependency creates tight coupling.
from abc import ABC, abstractmethod
class Subject:
def __init__(self):
self._observers: list[Observer] = []
self._state = None
def attach(self, observer): self._observers.append(observer)
def detach(self, observer): self._observers.remove(observer)
@property
def state(self): return self._state
@state.setter
def state(self, value):
self._state = value
self._notify()
def _notify(self):
for o in self._observers:
o.update(self)
class Observer(ABC):
@abstractmethod
def update(self, subject): ...
class EmailNotifier(Observer):
def update(self, subject):
print(f"Email: state changed to {subject.state}")
class Logger(Observer):
def update(self, subject):
print(f"Log: state = {subject.state}")
subject = Subject()
subject.attach(EmailNotifier())
subject.attach(Logger())
subject.state = "active"
When to use: event systems, model-view in UIs, pub/sub patterns, reactive programming.
Real-world: Django signals, JS DOM event listeners, RxPy / RxJS, Redis pub/sub.
Pitfalls:
- Memory leaks if observers aren’t detached.
- Ordering of observer notification.
- Cascading updates: observer A’s update triggers subject B, which has its own observers…
- Synchronous notification blocks the subject.
State
Intent: Allow an object to alter its behavior when its internal state changes. The object appears to change its class. Reference: https://refactoring.guru/design-patterns/state
Problem: A class has many if state == "X" branches in methods. Adding states requires touching every method.
from abc import ABC, abstractmethod
class State(ABC):
@abstractmethod
def insert_coin(self, machine): ...
@abstractmethod
def select_product(self, machine): ...
@abstractmethod
def dispense(self, machine): ...
class IdleState(State):
def insert_coin(self, machine):
print("Coin inserted")
machine.state = HasCoinState()
def select_product(self, machine): print("Insert coin first")
def dispense(self, machine): print("Insert coin first")
class HasCoinState(State):
def insert_coin(self, machine): print("Already has coin")
def select_product(self, machine):
print("Product selected")
machine.state = DispensingState()
def dispense(self, machine): print("Select product first")
class DispensingState(State):
def insert_coin(self, machine): print("Wait, dispensing")
def select_product(self, machine): print("Wait, dispensing")
def dispense(self, machine):
print("Product dispensed")
machine.state = IdleState()
class VendingMachine:
def __init__(self):
self.state: State = IdleState()
def insert_coin(self): self.state.insert_coin(self)
def select_product(self): self.state.select_product(self)
def dispense(self): self.state.dispense(self)
vm = VendingMachine()
vm.insert_coin()
vm.select_product()
vm.dispense()
State vs Strategy:
- Strategy: client picks an algorithm; algorithms are interchangeable.
- State: the object changes its own state based on internal events; states drive transitions.
Real-world: state machines (TCP connection lifecycle, document workflow), game character states, UI components.
Pitfalls: state explosion if you over-divide; states can become coupled if they reference each other to set transitions.
Strategy
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Reference: https://refactoring.guru/design-patterns/strategy
Problem: Multiple algorithms solve the same problem; client wants to pick at runtime. Hardcoded if algo == "X": blocks make adding algorithms invasive.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
class QuickSort(SortStrategy):
def sort(self, data): return sorted(data) # imagine real quicksort
class BubbleSort(SortStrategy):
def sort(self, data):
# actual bubble sort
data = data.copy()
for i in range(len(data)):
for j in range(len(data) - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
return data
class Context:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def execute(self, data):
return self.strategy.sort(data)
ctx = Context(QuickSort())
print(ctx.execute([3, 1, 2]))
ctx.strategy = BubbleSort() # swap at runtime
print(ctx.execute([3, 1, 2]))
Pythonic shortcut — strategies are often just functions:
def sort_with(data, strategy):
return strategy(data)
sort_with([3, 1, 2], sorted)
sort_with([3, 1, 2], lambda d: sorted(d, reverse=True))
When to use: many algorithms for one task; algorithm choice should be runtime-configurable; want to avoid conditionals on algorithm type.
Real-world: sorting/comparison functions, payment methods, compression algorithms, authentication backends, retry strategies.
Pitfalls: over-engineering — a function or callable often suffices; don’t create class hierarchies for two-line algorithms.
Template Method
Intent: Define the skeleton of an algorithm in a method, deferring some steps to subclasses. Subclasses redefine certain steps without changing the algorithm’s structure. Reference: https://refactoring.guru/design-patterns/template-method
Problem: Multiple subclasses share most of an algorithm with small variations. Duplicating the skeleton invites drift; pure inheritance for variation is rigid.
from abc import ABC, abstractmethod
class DataPipeline(ABC):
def process(self): # template method (final-ish)
data = self.extract()
transformed = self.transform(data)
self.load(transformed)
self.notify()
@abstractmethod
def extract(self): ...
@abstractmethod
def transform(self, data): ...
@abstractmethod
def load(self, data): ...
def notify(self): # hook with default
print("Pipeline complete")
class CSVPipeline(DataPipeline):
def extract(self):
return ["row1,a,b", "row2,c,d"]
def transform(self, data):
return [row.split(",") for row in data]
def load(self, data):
print(f"Loaded {len(data)} rows")
class JSONPipeline(DataPipeline):
def extract(self):
return '[{"x": 1}, {"x": 2}]'
def transform(self, data):
import json
return json.loads(data)
def load(self, data):
print(f"Loaded {len(data)} JSON objects")
CSVPipeline().process()
JSONPipeline().process()
Template Method vs Strategy:
- Template Method: inheritance — subclasses fill in steps of an algorithm.
- Strategy: composition — strategy objects are swappable.
When to use: algorithm structure is fixed, specific steps vary. Classic in frameworks (Django views, pytest fixtures, ETL pipelines).
Real-world: Django generic views, pytest hooks, frameworks where you override specific methods.
Pitfalls: deep inheritance hierarchies; “fragile base class” — changing the template breaks subclasses.
Visitor
Intent: Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements. Reference: https://refactoring.guru/design-patterns/visitor
Problem: You have a stable set of element classes (tree of AST nodes, document structure). Many different operations want to traverse them (render to HTML, render to PDF, type check, evaluate). Adding each operation as a method to every class is invasive.
from abc import ABC, abstractmethod
# Visitor interface
class Visitor(ABC):
@abstractmethod
def visit_number(self, number): ...
@abstractmethod
def visit_add(self, add): ...
# Elements accept a visitor
class Expression(ABC):
@abstractmethod
def accept(self, visitor): ...
class Number(Expression):
def __init__(self, value): self.value = value
def accept(self, visitor): return visitor.visit_number(self)
class Add(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def accept(self, visitor): return visitor.visit_add(self)
# Concrete visitors
class Evaluator(Visitor):
def visit_number(self, n): return n.value
def visit_add(self, a): return a.left.accept(self) + a.right.accept(self)
class Printer(Visitor):
def visit_number(self, n): return str(n.value)
def visit_add(self, a): return f"({a.left.accept(self)} + {a.right.accept(self)})"
expr = Add(Number(3), Add(Number(4), Number(5)))
print(expr.accept(Evaluator())) # 12
print(expr.accept(Printer())) # "(3 + (4 + 5))"
Double dispatch: the visitor pattern uses two dispatches — first on the element (element.accept(visitor)), then on the visitor (visitor.visit_X(element)). Selects behavior based on both types.
When to use: stable element hierarchy, many distinct operations over it. ASTs, document trees, syntax checkers, code generators.
Pitfalls:
- Adding a new element type requires updating ALL visitors. (Inverse of adding a new operation, which is cheap.)
- Verbose in Python (where simpler patterns suffice). Often replaced by
functools.singledispatchor pattern matching (match/case).
Pythonic alternative:
from functools import singledispatch
@singledispatch
def evaluate(node): raise NotImplementedError
@evaluate.register
def _(node: Number): return node.value
@evaluate.register
def _(node: Add): return evaluate(node.left) + evaluate(node.right)
Choosing among behavioral patterns
| Situation | Pattern |
|---|---|
| Pass request along a chain of handlers | Chain of Responsibility |
| Encapsulate operations (for undo, queue, logging) | Command |
| Interpret a small custom language | Interpreter |
| Traverse a collection | Iterator |
| Mediate complex object interactions | Mediator |
| Save/restore state without breaking encapsulation | Memento |
| Notify dependents when state changes | Observer |
| Behavior changes based on internal state | State |
| Interchangeable algorithms at runtime | Strategy |
| Algorithm skeleton with overridable steps | Template Method |
| Multiple operations over a stable element hierarchy | Visitor |
Pattern relationships
- Strategy vs State: same structure (object holds a strategy/state); intent differs. Strategy is “pick the algorithm”; State is “the object changes behavior as state transitions.”
- Strategy vs Template Method: composition (Strategy) vs inheritance (Template Method) for varying parts of an algorithm.
- Observer vs Mediator: Observer is one-to-many notification; Mediator is many-to-many routing.
- Command vs Strategy: Command is “what to do” as an object (with
execute()); Strategy is “how to do it” as a swappable algorithm. - Iterator + Composite: Iterator naturally traverses Composite trees.
- Visitor + Composite: Visitor often operates on Composite hierarchies (especially ASTs).
Common interview confusions
- “Strategy and State are the same.” — same structure (object holds a behavior object); different intent. Strategy is the client picking an algorithm; State is the object self-transitioning between behaviors.
- “Observer and Mediator are the same.” — Observer: subject → observers (one-to-many). Mediator: ↔ many objects with central routing. Different shapes.
- “Iterator is just a
forloop.” —foris consumer syntax; Iterator is the producer protocol (__iter__,__next__). Custom collections implement Iterator to befor-loopable. - “Visitor is the only way to traverse trees.” — visitor pattern is one way;
singledispatch, pattern matching, or just polymorphic methods often suffice in Python.
Interview angle
- “What are the GoF behavioral patterns?” — eleven patterns for communication and behavior: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.
- “Strategy vs State?” — same structure (composed behavior object), different intent. Strategy: client/external code picks an algorithm; algorithms are usually independent. State: the object transitions between states based on internal events; states often reference each other to drive transitions.
- “Observer vs Mediator?” — Observer broadcasts subject changes to many observers (one-to-many). Mediator centralizes communication among many objects (many-to-many with a central hub). Both reduce coupling but different shapes.
- “What’s the Command pattern used for?” — encapsulating an action as an object so it can be queued, logged, scheduled, or undone. Classic in GUI buttons, transaction systems, undo/redo, macro recording.
- “How do you implement Iterator in Python?” — define
__iter__(returning self or a new iterator) and__next__(returning next value or raisingStopIteration). Or just use a generator function — generators are iterators. - “When would you use Visitor?” — operations over a stable element hierarchy that you can’t or shouldn’t modify (AST, document tree). When new operations are added often but element types rarely change. In Python,
functools.singledispatchor pattern matching often replaces it. - “Template Method vs Strategy?” — Template Method uses inheritance (subclass overrides specific steps of a fixed algorithm). Strategy uses composition (different strategy objects swap entirely). Same problem (varying parts of an algorithm), different mechanism.
- “What’s Chain of Responsibility used for in real-world Python?” — middleware stacks. Django middleware, Flask before/after request handlers, FastAPI middleware: each handler decides to handle, modify, or pass the request.
- “Why is Interpreter less common today?” — modern projects use parser generators (PLY, lark) or built-in libraries for grammars. Hand-rolled interpreter classes don’t scale beyond toy DSLs.