Intuition What this page is for
The parent note told you what each behavioral pattern is. This page throws every kind of situation at those patterns — the easy case, the tricky case, the degenerate "one item / zero items" case, a real-world story, and an exam-style trap — and works each one out loud. If you can follow all eleven cells of the matrix below, you have actually used these patterns, not just read about them.
See the parent for definitions: Behavioral Design Patterns .
Definition The vocabulary this page uses (one line each, so the page stands alone)
Observer ::: a subject keeps a list of listeners and, when it changes, loops over them calling update(). One change → many notified.
Subject / Observer ::: the subject is the thing that changes and broadcasts; an observer (listener) is anything that reacts via an agreed update() method.
Strategy ::: an interchangeable algorithm passed into a context ; the context just calls it, never knowing which one it holds.
Context ::: the object that holds a strategy (or a state) and delegates work to it.
Command ::: a request wrapped as an object with execute() (and undo()), so it can be stored, queued, or reversed.
Iterator ::: an object that produces a collection's items one at a time; the traversal position lives inside the iterator, not the collection.
State ::: like Strategy in shape, but each state object triggers its own transition to the next state.
Template Method ::: a base class fixes the skeleton of an algorithm (the order of steps) in one method, and leaves individual steps (called hooks ) for subclasses to fill in — "don't call us, we'll call you."
Hook ::: a single overridable step inside a Template Method's skeleton; the base class calls it, the subclass fills it.
Chain of Responsibility ::: handlers wired in a line; each either handles a request or passes it to the next.
Before we solve anything, let us list every case class a behavioral-pattern problem can hand you. Think of this like listing all four quadrants before doing trigonometry — we want zero surprises later. The "Covered by" column gives a one-line summary so you can grasp the whole map without flipping.
Cell
Case class
What makes it tricky
Covered by (one-line result)
C1
Normal Observer fan-out
many listeners, order of notify
Ex 1 — 2 listeners print 2 lines, in subscribe order
C2
Degenerate: zero observers
notify() with an empty list
Ex 1 — empty list ⇒ loop runs 0 times, silent
C3
Unsubscribe during notify
list mutated while looping (the classic bug)
Ex 2 — raw loop skips a listener; copy the list to fix
C4
Strategy chosen at runtime
swap algorithm without an if
Ex 3 — same context, [1,2,3] vs [3,2,1]
C5
Strategy vs State confusion
same UML, different intent
Ex 4 — State self-mutates state; Strategy is inert
C6
Command undo — full & empty history
undo() when nothing is stacked
Ex 5 — undo returns doc to []; 3rd undo needs a guard
C7
Iterator: two independent walks
state must live in the iterator
Ex 6 — both walks give [3,2,1], no interference
C8
Iterator: empty collection
loop body runs zero times
Ex 6 — Countdown(0) yields []
C9
State machine: legal vs illegal transition
ignore vs act
Ex 7 — Draft→Published, 2nd publish is a no-op
C10
Template Method: fixed skeleton, filled hook
subclass overrides a step, never the order
Ex 8 — Sales fills body(), order stays header/body/footer
C11
Real-world + exam twist: Chain of Responsibility
who handles, who passes, nobody handles
Ex 9 — L1/Manager handle; level-4 falls off the end
We will now walk every cell.
A temperature Sensor (the subject ) has two listeners: a Logger and a Display. We subscribe both, push "temp=42", then create a fresh sensor with no listeners and push "temp=0". What prints in each case, and in what order?
Forecast: guess before reading — how many lines print for the first push? For the second?
Here is the subject, so nothing is used before it exists. The line self._observers = [] is the whole trick: the subject initializes an empty list in its constructor and owns it forever after.
class Sensor :
def __init__ (self):
self ._observers = [] # created empty right here, in __init__
def subscribe (self, o): self ._observers.append(o)
def notify (self, data):
for o in self ._observers: # loop over whatever is in the list
o.update(data)
Build the subject; __init__ sets self._observers = [].
Why this step? The subject must own the list of listeners so it can loop over them — the whole point of Observer is that the subject talks to listeners only through the update() interface. self._observers is defined here , in the constructor, before any other line touches it.
Subscribe Logger then Display. Each subscribe appends, so the list is now [Logger, Display], in insertion order .
Why this step? Python lists preserve order, so notify() will call them in the order they subscribed — Logger first.
sensor.notify("temp=42") loops the list and calls update("temp=42") on each.
Why this step? Fan-out = one event reaches many listeners with no if in sight. Output:
LOG: temp=42
DISPLAY: temp=42
That is 2 lines , Logger before Display (cell C1 ).
Fresh sensor, empty list, notify("temp=0"). The for o in self._observers: loop iterates over [] (still just the [] that __init__ created, since nobody subscribed).
Why this step? The degenerate case. A loop over an empty list runs its body zero times — no crash, no output. This is cell C2 , and it's why Observer needs no special "do I have listeners?" guard.
Verify: total lines printed across both pushes is 2 then 0 , so 2 in all . If you forecast 2 then 0, you understood that notify is just a loop, and an empty loop is silent.
Three listeners subscribe in order: [A, Logger, Display]. On its first update, Logger unsubscribes itself . If notify naïvely does for o in self._observers: o.update(...), which listeners actually get notified — and does Display get skipped?
Forecast: will Display receive the event, or get skipped?
Loop at index 0 → A.update runs. List is still [A, Logger, Display]; A is delivered.
Why this step? Before any mutation, the loop behaves normally — establishes the baseline.
Loop advances to index 1 → Logger.update runs , which calls self._observers.remove(logger).
Why this step? We are mutating the list we are iterating. Removing Logger (at index 1) shifts Display from index 2 down to index 1 — the very slot the loop just finished.
Loop advances to index 2 — but the list is now [A, Display] (length 2), so index 2 is out of range and the loop ends . Display, now sitting at index 1, was jumped over and never notified.
Why this step? This is the bug: mutate-while-iterate silently drops the listener that shifted into the just-visited slot. Deliveries to Display: 0 .
The fix: iterate over a copy : for o in list(self._observers):.
Why this step? The copy is a frozen snapshot of [A, Logger, Display]; removals from the real list don't shift the copy's indices, so the loop still visits Display. Deliveries to Display: 1 .
Verify: with the raw loop, delivered set is {A} for the tail (Display count 0); with the copy fix, Display count is 1. The difference of exactly 1 is the dropped listener — cell C3 .
"It worked on my machine" — because the bug only bites when a listener unsubscribes itself during notify . Snapshot the list every time and it never happens.
A Context sorts a list using whatever strategy it's given. Given data = [3, 1, 2], show the output for the ascending strategy sorted and for the descending strategy lambda d: sorted(d, reverse=True).
Forecast: what two lists come out?
Context(sorted).do([3,1,2]) — the context calls self.strategy(data), i.e. sorted([3,1,2]).
Why this step? Strategy replaces if mode=="asc" with passing the algorithm itself . In Python a function already is a swappable object. Result: [1, 2, 3].
Context(lambda d: sorted(d, reverse=True)).do([3,1,2]).
Why this step? Same context, same call site, different behavior — the context never learns which algorithm it holds. Result: [3, 2, 1].
Verify: ascending gives [1,2,3], descending gives [3,2,1]; they are exact reverses of each other, confirming the context truly delegated (cell C4 ). See also Polymorphism — Strategy is polymorphism by composition.
Two code snippets both have a "context holds an object, delegates to it." One is Strategy, one is State. Decide which is which:
Snippet A: Context(sorted) and Context(reverse_sort) — the client picks one, they never change themselves.
Snippet B: a Draft object that, on publish(), sets ctx.state = Published().
Forecast: which snippet transitions itself?
Snippet A: strategies are inert and independent. No strategy sets the next strategy; the client chooses once.
Why this step? Intent test — Strategy = "pick an algorithm." The objects don't know about each other.
Snippet B: Draft.publish writes ctx.state = Published().
Why this step? The state itself triggers the transition — that is the defining difference. Draft knows the next state.
Verify (behavioral proof): call publish() on a Doc twice. If it's State, the object's state field changes from Draft to Published (a transition happened). If it were Strategy, nothing self-changes. So:
after 1st publish: state is Published
after 2nd publish: prints "already live", state stays Published.
The self-mutation of state is the fingerprint of State (cell C5 ). Compare with SOLID Principles — both patterns honour Open/Closed by adding classes, not ifs.
Start with empty doc = []. Run AddText(doc, "A"), then AddText(doc, "B"). Then call undo() three times. What is doc after each step, and what happens on the third undo?
Forecast: after two adds and two undos, what's in doc? What does the third undo do?
First, here are the pieces so nothing is used before it's defined. A command is an object that knows how to execute() and undo() itself. The tiny helper run() is the invoker : it executes a command and remembers it by pushing it onto a history stack, so we can reverse it later.
class AddText :
def __init__ (self, doc, text): self .doc, self .text = doc, text
def execute (self): self .doc.append( self .text)
def undo (self): self .doc.pop()
history = [] # the undo stack, defined once up front
def run (cmd):
cmd.execute() # do the work now
history.append(cmd) # remember it, so undo() can find it later
def undo ():
if history: # guard: only undo if something is stacked
history.pop().undo() # take the newest command, reverse it
run(AddText(doc,"A")) → run calls execute() (appends "A") then pushes the command → doc = ["A"], history = [cmdA].
Why this step? Command turns the verb "add A" into a stored noun. run is what stores it, so history now holds a reversible object.
run(AddText(doc,"B")) → doc = ["A","B"], history = [cmdA, cmdB].
Why this step? run pushes a second command onto the stack. Because each command is a stored object, the history is now an ordered stack we can peel back one at a time — this stacking is what makes multi-step undo possible.
undo() → history.pop() returns cmdB, whose undo() pops "B" → doc = ["A"], history = [cmdA].
Why this step? The command stored enough to reverse itself ; LIFO undo = Ctrl-Z. The most recent command is undone first.
undo() → pops cmdA, its undo() pops "A" → doc = [], history = [].
Why this step? We continue peeling the stack. Undoing in reverse order (cmdB then cmdA) exactly retraces the edits backwards, returning doc to its original empty state — proving undo is the true inverse of execute.
undo() a third time (empty history — the degenerate cell C6). The if history: guard is now false, so undo() does nothing (no IndexError). Without that guard, history.pop() on an empty list would raise IndexError.
Why this step? This is the boundary you must handle: undo with nothing to undo. The guard is why the third call is a safe no-op.
Verify: doc sequence is [] → ["A"] → ["A","B"] → ["A"] → [] . Adds and undos are symmetric, so len(doc) returns to 0. The third undo is a guarded no-op (cell C6 ). This is the engine behind Undo-Redo Systems .
Countdown(3) yields 3,2,1. (a) Run two for loops over the same Countdown(3) object and show they don't interfere. (b) Run a for loop over Countdown(0). How many times does the loop body run?
Forecast: does the second loop start again at 3, or continue where the first stopped? How many iterations for Countdown(0)?
First loop: for x in Countdown(3) — __iter__ runs, creating a fresh local i = 3 and yielding 3, 2, 1.
Why this step? Traversal state (i) lives in the iterator , not the collection. Each for calls __iter__ and gets a brand-new counter.
Second loop: another for over the same object again enters __iter__, again i = 3, again yields 3, 2, 1.
Why this step? Because state is per-iterator, the two walks are fully independent (cell C7 ). Both produce [3, 2, 1].
Countdown(0): __iter__ sets i = 0; the while i > 0: is immediately false , so it yields nothing.
Why this step? The degenerate empty case — the loop body executes 0 times , no error (cell C8 ), exactly like Observer's empty fan-out.
Verify: list(Countdown(3)) == [3,2,1] for both walks; sum(1 for _ in Countdown(3)) == 3 iterations; list(Countdown(0)) == [] → 0 iterations. Independence + empty case confirmed.
A Doc starts as Draft. We call publish() (legal: Draft → Published), then publish() again (now illegal: already Published). Trace the state field and any printout.
Forecast: after two publishes, is the doc Published once, twice, or broken?
Here is the machine. Each state is its own class; the Doc (the context ) just forwards publish() to whatever state it currently holds.
class Draft :
def publish (self, ctx): ctx.state = Published() # legal move: switch state
class Published :
def publish (self, ctx): print ( "already live" ) # illegal: absorb, no switch
class Doc :
def __init__ (self): self .state = Draft() # start state
def publish (self): self .state.publish( self ) # delegate to current state
Alt text / how to read it: two circles — Draft (left, chalk-blue, marked "start state") and Published (right, pale-yellow). A single yellow arrow points Draft → Published labelled publish() = legal. A pink self-loop curls back onto Published, labelled publish() again = 'already live'. Legal moves cross to a new node; the illegal move loops back to the same node.
Initial: Doc.__init__ sets self.state = Draft() — the start node (left circle).
Why this step? Every state machine needs a well-defined starting state, or the first method call has nothing to delegate to. In the figure this is the node marked "start state."
First publish(): Doc.publish delegates to Draft.publish(self), which runs ctx.state = Published() — the yellow left-to-right arrow in the figure.
Why this step? The state object performs the transition itself (that's what separates State from Strategy). This is a legal edge, so the machine advances Draft → Published, and Doc.publish was never edited.
Second publish(): state is now Published, so Doc.publish delegates to Published.publish, which just prints "already live" and does not touch state — the pink self-loop on the Published node.
Why this step? An illegal transition: instead of crashing, the state absorbs the request and stays put. Adding a new status later = adding a class, never editing Doc.publish.
Verify: after step 2 the state is Published; after step 3 it is still Published (idempotent). Exactly one legal transition happened; the second publish changed nothing (cell C9 ), matching the single arrow + self-loop in the figure.
A base Report freezes the order header() → body() → footer() inside a generate() method. header and footer are shared; body is a hook left blank. A subclass Sales fills only body() to print "sales: 100". What does Sales().generate() print, and in what order?
Forecast: guess the exact three lines and their order before reading — and ask yourself: can Sales reorder them?
Here is the skeleton and the subclass. Notice Sales overrides one method and never touches generate().
class Report :
def generate (self): # the TEMPLATE — fixed skeleton
self .header(); self .body(); self .footer()
def header (self): print ( "=== Report ===" )
def body (self): raise NotImplementedError # the HOOK — subclass fills it
def footer (self): print ( "--- end ---" )
class Sales ( Report ):
def body (self): print ( "sales: 100" ) # fill ONLY the hook, order untouched
Sales().generate() runs the base-class generate. Sales never overrode generate, so it inherits the skeleton unchanged.
Why this step? This is "inversion of control": the base class calls the steps in a frozen order — "don't call us, we'll call you." The subclass cannot reorder them because it doesn't own the calling method.
self.header() runs — Sales didn't override it, so the base version prints === Report ===.
Why this step? Shared steps live once in the base class; no copy-paste. This is the 80% that both reports share.
self.body() runs — here Sales's override wins, printing sales: 100.
Why this step? The hook is the one varying step. Polymorphism (Polymorphism ) sends self.body() to the subclass's version — the only thing the subclass changed.
self.footer() runs — inherited base version prints --- end ---.
Why this step? The skeleton always ends the same way. Order is guaranteed: header, then body, then footer, no matter which subclass.
Verify: output is exactly three lines in order:
=== Report ===
sales: 100
--- end ---
The first and third lines came from the base class, the middle from the subclass — proving the hook varied while the skeleton held (cell C10 ).
Common mistake Strategy vs Template Method
Both let one step vary. Strategy varies it by composition (pass an object into a context); Template Method varies it by inheritance (override a method). If you're subclassing to change one step of a fixed sequence , it's Template Method.
A support ticket with level goes through handlers L1 (handles level ≤ 1), L2 (handles level ≤ 2), Manager (handles level ≤ 3) , wired in that order. Route: (a) a level-1 ticket, (b) a level-3 ticket, (c) an exam twist — a level-4 ticket that no one can handle.
Forecast: which handler answers each ticket? What happens to the level-4 ticket?
Alt text / how to read it: three circles in a row — L1 and L2 (chalk-blue), then Manager (pale-yellow), each labelled with the level it handles. White "pass" arrows connect them left to right. A pink arrow runs off the right edge past Manager, labelled next=None, ending at the word "dropped!" — that dangling edge is where an unhandled ticket falls off the chain.
Each handler follows the same rule: "if I can handle it, do so; else pass to self.next."
Level-1 ticket hits L1 . Condition level ≤ 1 is true → L1 handles , chain stops.
Why this step? Chain of Responsibility lets the first capable handler answer; the sender never knew who it would be. Zero passes occurred — L1 is the leftmost circle in the figure.
Level-3 ticket hits L1 (3 ≤ 1? no → pass) → L2 (3 ≤ 2? no → pass) → Manager (3 ≤ 3? yes → handle ).
Why this step? Escalation is just "pass to next" repeated — the two white arrows in the figure. Two passes, then handled; the request walked the chain until someone qualified.
Level-4 ticket (the twist) passes L1 → L2 → Manager (4 ≤ 3? no) → self.next is None (the pink dangling arrow in the figure).
Why this step? The degenerate end-of-chain case. Nobody handled it. Robust chains add a default/fallback handler; otherwise the ticket is silently dropped — the bug to watch for (cell C11 ).
Verify:
level 1 → handled by L1 (0 passes)
level 3 → handled by Manager (2 passes)
level 4 → handled by nobody (3 passes, falls off the end)
The number of passes equals (index of the handler that answered) , and level 4 reaching a pass-count of 3 with no handler proves the fall-through. This mechanism generalises to Publish-Subscribe & Event-Driven Architecture middleware chains.
Recall Self-test: cover the answers
An Observer's notify loops the raw list and a listener unsubscribes itself — what breaks? ::: The listener that shifts into the just-visited slot is skipped (index shift); fix by iterating over a copy.
undo() on empty history does what? ::: With the if history: guard, it's a safe no-op; without it, raises IndexError.
Two for loops over one Iterator object — do they interfere? ::: No; each for calls __iter__ and gets fresh, independent state.
In Template Method, what can a subclass change and what can't it? ::: It fills the hooks (individual steps) but cannot change the order — the base class owns the skeleton.
A level-4 ticket in a chain that maxes at level 3? ::: Falls off the end unhandled — needs a fallback handler.
Same UML for Strategy and State — what distinguishes them? ::: State objects trigger their own transitions; strategies are inert and client-chosen.
Mnemonic One line per pattern's edge case
O bserver empties silently · S trategy never self-switches · C ommand can undo nothing (guard it) · I terator empties silently · S tate ignores illegal moves · T emplate fixes the order · C hain can end unhandled.