Intermediate
Examples 29-58 solve four recurring problems -- word frequency, a turnstile state machine, and a
paradigm-cost measurement -- across all four major paradigms side by side, then go deep on logic
programming (co-13, co-14), constraint programming (co-15), event-driven and reactive systems (co-16,
co-17), dataflow (co-18), relational thinking (co-19), and the discipline of mixing paradigms cleanly at
a boundary (co-25). Every example is self-contained under learning/code/ex-NN-slug/.
Example 29: Four Ways -- Imperative
ex-29 · exercises co-01
The first of a four-example set solving one word-frequency task in imperative, OO, functional, and declarative style -- all four must agree on the identical output dict.
example.py
"""Example 29: Four Ways -- Imperative."""
def word_frequency_imperative(text: str) -> dict[str, int]: # => way #1 of 4: loop + dict, mutated in place
counts: dict[str, int] = {} # => mutable accumulator
for word in text.split(): # => explicit iteration
counts[word] = counts.get(word, 0) + 1 # => explicit mutation, one word at a time
return counts # => the final mutated state
sample = "red blue red green blue red" # => shared sample text for all four "four-ways" examples
result = word_frequency_imperative(sample) # => run it
print(result) # => red: 3, blue: 2, green: 1
# => Output: {'red': 3, 'blue': 2, 'green': 1}Run
python3 example.pyOutput
{'red': 3, 'blue': 2, 'green': 1}test_example.py
"""Example 29: pytest verification for Four Ways -- Imperative."""
from example import word_frequency_imperative
def test_counts_match_the_known_sample() -> None:
assert word_frequency_imperative("red blue red green blue red") == {
"red": 3,
"blue": 2,
"green": 1,
} # => shared expected result across examples 29-32
# => Run: pytest -- Output: 1 passedVerify
pytest -qOutput
1 passedKey takeaway: a mutable dict and an explicit loop compute the word frequency in the most direct
way Python offers -- the baseline the next three examples measure themselves against.
Why it matters: this same task, solved three more ways over the next three examples, is this
topic's clearest side-by-side demonstration that paradigm choice changes the shape of the code, never
the correctness of the answer. Compare word_frequency_imperative()'s three-line loop body to Example 31's one-line Counter call: both produce the identical {'red': 3, 'blue': 2, 'green': 1}, so any preference between them has to be argued on readability or testability, not correctness.
Example 30: Four Ways -- OO
ex-30 · exercises co-05
The same word-frequency task, this time bundled into a WordFrequencyCounter class whose count()
method returns self for chaining.
example.py
"""Example 30: Four Ways -- OO."""
class WordFrequencyCounter: # => way #2 of 4: state and behavior bundled in a class
def __init__(self) -> None: # => constructor runs once, before count() is ever called
self._counts: dict[str, int] = {} # => private state, only this class's methods touch it
# => starts empty -- every instance gets its own independent dict, never shared
def count(self, text: str) -> "WordFrequencyCounter": # => behavior: process text, mutate self
for word in text.split(): # => same tokenization as example 29
self._counts[word] = self._counts.get(word, 0) + 1 # => mutate this instance's own state
return self # => returning self allows chaining, a common OO idiom
def result(self) -> dict[str, int]: # => behavior: read this instance's own state
return dict(self._counts) # => defensive copy
sample = "red blue red green blue red" # => identical sample to example 29
counter = WordFrequencyCounter().count(sample) # => construct, then chain the count() call
print(counter.result()) # => must match example 29's dict exactly
# => Output: {'red': 3, 'blue': 2, 'green': 1}Run
python3 example.pyOutput
{'red': 3, 'blue': 2, 'green': 1}test_example.py
"""Example 30: pytest verification for Four Ways -- OO."""
from example import WordFrequencyCounter
def test_oo_counts_match_the_imperative_version() -> None:
result = WordFrequencyCounter().count("red blue red green blue red").result()
assert result == {"red": 3, "blue": 2, "green": 1} # => identical to example 29's result
# => Run: pytest -- Output: 1 passedVerify
pytest -qOutput
1 passedKey takeaway: WordFrequencyCounter().count(sample) chains construction and mutation into one
expression, but the tally itself still lives as private state on the instance -- the same isolation
Example 6 demonstrated.
Why it matters: the OO version's answer is byte-identical to the imperative version's, confirming
paradigm choice here is a question of code shape and testability, not correctness. The chaining idiom WordFrequencyCounter().count(sample) also lets a caller add more input in a second call without re-declaring a fresh dict, a convenience the plain word_frequency_imperative() function does not offer without extra parameters.
Example 31: Four Ways -- Functional
ex-31 · exercises co-09
The same word frequency computed with collections.Counter in one expression -- no accumulator, no
class, no explicit mutation anywhere.
example.py
"""Example 31: Four Ways -- Functional."""
from collections import Counter # => way #3 of 4: a value-producing call, no visible mutation
sample = "red blue red green blue red" # => identical sample to examples 29-30
result = dict(Counter(sample.split())) # => one expression: tokenize, then fold into counts
print(result) # => must match examples 29-30's dict exactly
# => Output: {'red': 3, 'blue': 2, 'green': 1}Run
python3 example.pyOutput
{'red': 3, 'blue': 2, 'green': 1}test_example.py
"""Example 31: pytest verification for Four Ways -- Functional."""
from collections import Counter
def word_frequency_functional(text: str) -> dict[str, int]: # => reusable helper mirroring example.py
return dict(Counter(text.split())) # => value-producing, no mutation of the caller's input
def test_functional_counts_match_the_other_three_ways() -> None:
assert word_frequency_functional("red blue red green blue red") == {
"red": 3,
"blue": 2,
"green": 1,
} # => identical to examples 29-30's result
# => Run: pytest -- Output: 1 passedVerify
pytest -qOutput
1 passedKey takeaway: dict(Counter(sample.split())) is the entire computation -- one expression, no
accumulator variable, no class, no visible mutation step.
Why it matters: Counter is a value-producing tool by design -- it never mutates the list you hand
it, which is exactly co-09's functional-paradigm discipline in a single stdlib call. Where Example 29's loop needs three lines -- initialize, iterate, mutate -- dict(Counter(sample.split())) reaches the identical {'red': 3, 'blue': 2, 'green': 1} in one expression, with the counting logic hidden entirely inside the stdlib's own C implementation.
Example 32: Four Ways -- Declarative
ex-32 · exercises co-08, co-19
The fourth and final version: GROUP BY word ORDER BY COUNT(*) DESC states the word-frequency result
declaratively, and SQLite computes it.
example.py
"""Example 32: Four Ways -- Declarative."""
import sqlite3 # => the standard library's built-in SQL engine -- no external dependency needed
sample = "red blue red green blue red" # => identical sample to examples 29-31
conn = sqlite3.connect(":memory:") # => way #4 of 4: state the desired result, let SQLite compute it
conn.execute("CREATE TABLE words (word TEXT)") # => declare the shape of the data
conn.executemany("INSERT INTO words VALUES (?)", [(w,) for w in sample.split()]) # => load every word
rows = conn.execute( # => the query IS the algorithm -- no accumulator variable anywhere in this file
"SELECT word, COUNT(*) FROM words GROUP BY word ORDER BY COUNT(*) DESC"
# => GROUP BY + COUNT(*) declares "the frequency of each word" -- no loop mechanics anywhere
).fetchall() # => the query planner decided HOW to group and count; this call only asks for the rows
result = dict(rows) # => turn the declared result into the same dict shape as examples 29-31
conn.close() # => release the connection
# => an in-memory connection's data disappears once closed -- nothing to clean up on disk
print(result) # => must match examples 29-31's dict exactly, across all four paradigms
# => Output: {'red': 3, 'blue': 2, 'green': 1}Run
python3 example.pyOutput
{'red': 3, 'blue': 2, 'green': 1}test_example.py
"""Example 32: pytest verification for Four Ways -- Declarative."""
import sqlite3
def word_frequency_declarative(text: str) -> dict[str, int]: # => reusable helper mirroring example.py
conn = sqlite3.connect(":memory:") # => fresh in-memory database per call
conn.execute("CREATE TABLE words (word TEXT)")
conn.executemany("INSERT INTO words VALUES (?)", [(w,) for w in text.split()])
rows = conn.execute("SELECT word, COUNT(*) FROM words GROUP BY word ORDER BY COUNT(*) DESC").fetchall()
conn.close() # => always release the connection
return dict(rows) # => same shape as the other three ways
def test_declarative_counts_match_all_three_other_ways() -> None:
result = word_frequency_declarative("red blue red green blue red")
assert result == {"red": 3, "blue": 2, "green": 1} # => the same dict, all four paradigms agree
# => Run: pytest -- Output: 1 passedVerify
pytest -qOutput
1 passedKey takeaway: four completely different paradigms -- imperative loop, OO class, functional
Counter, and declarative SQL -- all compute the byte-identical {'red': 3, 'blue': 2, 'green': 1}.
Why it matters: this four-example set is a preview of Example 59's fuller version: paradigm choice
does not change what a program computes, only how the computation is expressed and what it costs to
read, test, or extend. Unlike the imperative, OO, and functional versions, this one never states an explicit sort order in Python at all -- ORDER BY COUNT(*) DESC hands that decision to SQLite's own query planner, which can pick an index-backed strategy the other three versions have no equivalent hook for.
Example 33: State Machine -- Imperative
ex-33 · exercises co-04
A turnstile modeled with a mutable module-level global and an if/elif chain of
"current-state-and-event" transitions.
example.py
"""Example 33: State Machine -- Imperative."""
state = "locked" # => MUTABLE GLOBAL: the turnstile's current state lives in one module-level box
events: list[str] = ["coin", "push", "push", "coin", "coin", "push"] # => a sequence to replay
def handle(event: str) -> None: # => mutates the global `state` directly, one transition-if at a time
global state # => explicit acknowledgement this function reaches outside itself
if state == "locked" and event == "coin": # => explicit transition-if #1
state = "unlocked" # => mutate in place
elif state == "locked" and event == "push": # => explicit transition-if #2
pass # => pushing a locked turnstile does nothing -- state stays "locked"
elif state == "unlocked" and event == "push": # => explicit transition-if #3
state = "locked" # => mutate in place
elif state == "unlocked" and event == "coin": # => explicit transition-if #4
pass # => an extra coin on an already-unlocked turnstile changes nothing
history: list[str] = [state] # => record the state after every event, starting with the initial one
for event in events: # => replay every event against the mutable global
handle(event) # => each call may mutate `state`
history.append(state) # => record what it became
print(history) # => locked -> unlocked (coin) -> locked (push) -> ... -> locked -> unlocked
# => Output: ['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']Run
python3 example.pyOutput
['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']test_example.py
"""Example 33: pytest verification for State Machine -- Imperative."""
import example
def test_locked_to_unlocked_to_locked_sequence() -> None:
# => the module-level demo already replayed coin,push,push,coin,coin,push -- verify its trace
assert example.history == ["locked", "unlocked", "locked", "locked", "unlocked", "unlocked", "locked"]
def test_pushing_a_locked_turnstile_does_not_unlock_it() -> None:
example.state = "locked" # => reset the shared global explicitly for this test's own run
example.handle("push") # => push while locked
assert example.state == "locked" # => must remain locked -- pushing alone never unlocks
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: every transition rule is a separate if/elif branch checking both the current
state global and the incoming event -- correct, but the state lives outside any single function.
Why it matters: this is the baseline Examples 34 and 35 re-solve as an OO State pattern and a pure
fold respectively -- watch where the mutable global goes in each version. A bug where some unrelated code mutates state directly, bypassing handle()'s guarded transitions entirely, would compile and run without any error here -- exactly the risk Example 34's State pattern and Example 35's pure fold both close off structurally.
Example 34: State Machine -- OO (State Pattern)
ex-34 · exercises co-05
The identical turnstile, modeled with the State pattern: each state is its own object, and a transition returns a different state object rather than mutating a shared variable.
%% Color Palette: Blue #0173B2, Orange #DE8F05
graph LR
L["Locked"]:::blue -->|"on_coin()"| U["Unlocked"]:::orange
U -->|"on_push()"| L
L -->|"on_push() -> self"| L
U -->|"on_coin() -> self"| U
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 34: State Machine -- OO (State Pattern)."""
from abc import ABC, abstractmethod # => ABC/abstractmethod force every concrete state to implement both
class TurnstileState(ABC): # => the State pattern: each state is its OWN object, not a string tag
@abstractmethod # => marks on_coin() as required -- TurnstileState itself can never be instantiated
def on_coin(self) -> "TurnstileState": # => every state must say what a coin does to it
... # => no body here -- only concrete subclasses below provide the real behavior
@abstractmethod # => marks on_push() as required, same contract as on_coin() above
def on_push(self) -> "TurnstileState": # => every state must say what a push does to it
... # => no body here -- only concrete subclasses below provide the real behavior
name: str # => a human-readable label, set by each concrete subclass
class Locked(TurnstileState): # => concrete state object #1
name = "locked" # => satisfies the abstract `name` field declared on TurnstileState
def on_coin(self) -> TurnstileState: # => Locked's OWN answer to "what does a coin do?"
return Unlocked() # => transition: return a DIFFERENT state object
def on_push(self) -> TurnstileState: # => Locked's OWN answer to "what does a push do?"
return self # => stay locked -- return the same state object, unchanged
class Unlocked(TurnstileState): # => concrete state object #2
name = "unlocked" # => satisfies the same abstract `name` field, with this state's own value
def on_coin(self) -> TurnstileState: # => Unlocked's OWN answer to a coin
return self # => an extra coin changes nothing
def on_push(self) -> TurnstileState: # => Unlocked's OWN answer to a push
return Locked() # => transition back
events: list[str] = ["coin", "push", "push", "coin", "coin", "push"] # => same sequence as example 33
current: TurnstileState = Locked() # => start locked, as an OBJECT, not a string
history: list[str] = [current.name] # => record the starting state's name
for event in events: # => replay the same events
current = current.on_coin() if event == "coin" else current.on_push() # => dispatch via the object itself
history.append(current.name) # => record the new state's name
print(history) # => must be identical to example 33's trace
# => no `if state == "locked"` anywhere -- transitions live inside each state object's own methods
# => Output: ['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']Run
python3 example.pyOutput
['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']test_example.py
"""Example 34: pytest verification for State Machine -- OO (State Pattern)."""
from example import Locked, TurnstileState, Unlocked
def test_state_pattern_trace_matches_the_imperative_version() -> None:
events = ["coin", "push", "push", "coin", "coin", "push"] # => same sequence as example 33
current: TurnstileState = Locked() # => start locked
history = [current.name]
for event in events:
current = current.on_coin() if event == "coin" else current.on_push()
history.append(current.name)
assert history == ["locked", "unlocked", "locked", "locked", "unlocked", "unlocked", "locked"]
def test_each_transition_returns_a_distinct_state_object() -> None:
locked = Locked() # => construct once
unlocked = locked.on_coin() # => transition via a coin
assert isinstance(unlocked, Unlocked) # => coin from Locked always yields an Unlocked object
assert unlocked.on_push().name == "locked" # => push from Unlocked always yields back to locked
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: Locked.on_coin() returns a brand-new Unlocked() object rather than mutating a
shared state variable -- the "current state" is just whichever object current happens to point at.
Why it matters: no global exists anywhere in this version -- the State pattern relocates the fault
line from Example 33's mutable module variable to object identity, with each transition rule owned by
the state it starts from. Adding a third state -- say, a maintenance mode -- means writing one new TurnstileState subclass with its own on_coin()/on_push(), the same additive extension cost Example 27 showed for OO shapes, rather than editing every branch of Example 33's if/elif chain.
Example 35: State Machine -- Functional
ex-35 · exercises co-09
The same turnstile once more, as a pure transition(state, event) -> new_state function folded over
the event list with functools.reduce.
example.py
"""Example 35: State Machine -- Functional."""
from functools import reduce # => reduce() is Python's built-in fold: combine a sequence into one value
def transition(state: str, event: str) -> str: # => a PURE function: (state, event) -> new state
if state == "locked" and event == "coin": # => same rules as examples 33-34, expressed as pure data-in data-out
return "unlocked" # => no assignment to any outer variable -- just a returned value
if state == "unlocked" and event == "push": # => the other real transition
return "locked" # => same shape: a returned value, nothing mutated
return state # => every other combination is a no-op -- return the SAME state, no mutation anywhere
events: list[str] = ["coin", "push", "push", "coin", "coin", "push"] # => same sequence as examples 33-34
# => a FOLD builds the whole history in one expression -- no loop body visibly mutates anything
history = reduce( # => reduce(fn, sequence, initial) threads an accumulator through every event
lambda states, event: states + [transition(states[-1], event)], # => append the next state, functionally
events, # => the sequence being folded over, one event per step
["locked"], # => the fold's starting accumulator: history begins with just the initial state
) # => the final accumulator value IS the complete, fully-built history
print(history) # => must be identical to examples 33-34's trace
# => Output: ['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']Run
python3 example.pyOutput
['locked', 'unlocked', 'locked', 'locked', 'unlocked', 'unlocked', 'locked']test_example.py
"""Example 35: pytest verification for State Machine -- Functional."""
from example import transition
def test_pure_transition_matches_the_other_two_versions() -> None:
from functools import reduce
events = ["coin", "push", "push", "coin", "coin", "push"] # => same sequence as examples 33-34
history = reduce(lambda states, event: states + [transition(states[-1], event)], events, ["locked"])
assert history == ["locked", "unlocked", "locked", "locked", "unlocked", "unlocked", "locked"]
def test_transition_never_mutates_its_string_arguments() -> None:
before_state, before_event = "locked", "coin" # => strings are immutable in Python regardless,
result = transition(before_state, before_event) # => but this documents the pure-function contract
assert before_state == "locked" and before_event == "coin" # => arguments are provably unchanged
assert result == "unlocked" # => and the correct new state was returned as a NEW value
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: transition() has no global, no object state, and no mutation -- reduce threads
each new state through the fold as a plain returned value, producing the identical six-state trace.
Why it matters: three examples, three completely different homes for "where does the current state
live" (a global, an object, nowhere at all) -- and all three produce the exact same trace, which is
co-22's fault line made concrete across a whole mini-system. transition() alone can be unit-tested with a single call and no setup at all, unlike Example 33's handle(), which needs the shared global reset before every test to avoid leaking state between test cases.
Example 36: Prolog-in-Python (Unification + Backtracking)
ex-36 · exercises co-13, co-14
The same grandparent query from Example 19, this time resolved by an explicit search with two nested
choice points and a continue-based backtrack, rather than a single comprehension.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["grandparent('alice')"]:::blue --> B["try Y = 'bob' (parent alice bob)"]:::orange
B --> C["try Z = 'carol' (parent bob carol)"]:::teal
C --> D["unify! yield 'carol'"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 36: Prolog-in-Python (Unification + Backtracking)."""
from collections.abc import Iterator # => grandparent() below is a generator, typed as an Iterator
Fact = tuple[str, str] # => a (parent, child) fact, mirroring example 19's family data
facts: list[Fact] = [ # => the FACTS -- raw data, no rule about grandparents anywhere in this list
("alice", "bob"), # => alice is bob's parent
("bob", "carol"), # => bob is carol's parent
("carol", "dave"), # => carol is dave's parent
] # => the same three-generation family as example 19, this time queried via search, not a comprehension
def parent(x: str, y: str) -> bool: # => the base relation: is (x, y) a stored fact? (unification step)
return (x, y) in facts # => "unifying" x and y against every stored fact
def grandparent(x: str) -> Iterator[str]: # => the RULE, expressed as a search over intermediate variables
for _px, y in facts: # => try binding Y to every fact's child (a backtracking choice point)
if _px != x: # => this choice point only matters when x is actually the parent in this fact
continue # => BACKTRACK: this binding of Y didn't unify with parent(x, Y) -- try the next one
for py, z in facts: # => a NESTED choice point: try binding Z to every fact's child
if py != y: # => does this fact's parent match the Y we just bound?
continue # => BACKTRACK again: try the next candidate fact
yield z # => both parent(x, Y) and parent(Y, Z) unified -- z is a valid answer
results = list(grandparent("alice")) # => search: alice -> bob (bind Y) -> bob -> carol (bind Z)
# => draining the generator forces the backtracking search to actually run to completion
print(results) # => must match example 19's comprehension-based answer
# => Output: ['carol']Run
python3 example.pyOutput
['carol']test_example.py
"""Example 36: pytest verification for Prolog-in-Python (Unification + Backtracking)."""
from example import grandparent
def test_grandparent_query_resolves_via_search() -> None:
assert list(grandparent("alice")) == ["carol"] # => same answer as example 19's comprehension version
def test_a_person_with_no_grandchildren_yields_nothing() -> None:
assert list(grandparent("dave")) == [] # => dave has no recorded children at all -- search finds none
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: each continue is a genuine backtrack -- a rejected binding of y or z abandons
that branch of the search entirely and moves on to the next candidate fact.
Why it matters: Example 19's comprehension hid the same two nested choice points inside its for
clauses; writing them out explicitly here makes the unify-then-backtrack mechanics visible before
Example 37 scales the same shape to a much harder search. The two continue statements are the entire backtracking mechanism spelled out by hand -- Example 19's comprehension performs the identical search, but Python's own for clause machinery hides the same reject-and-try-next-candidate step inside syntax rather than an explicit statement.
Example 37: Backtracking N-Queens
ex-37 · exercises co-14
The classic N-Queens puzzle: place a queen, recurse, and if the rest of the board becomes unsolvable, pop the queen and try the next column.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["backtrack(row=0)"]:::blue --> B["try col=0: is_safe? place"]:::blue
B --> C["backtrack(row=1)"]:::orange
C -->|"no safe column"| D["pop queen, try col=1 at row=0"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 37: Backtracking N-Queens."""
def solve_n_queens(n: int) -> list[int] | None: # => returns one solution: cols[row] = column of the queen
cols: list[int] = [] # => the partial (and eventually full) placement, one column index per row
def is_safe(row: int, col: int) -> bool: # => can a queen go at (row, col) given queens placed so far?
for placed_row, placed_col in enumerate(cols): # => check against every already-placed queen
if placed_col == col: # => same column: an attack
return False # => reject immediately -- no need to check any other placed queen
if abs(placed_col - col) == abs(placed_row - row): # => same diagonal: an attack
return False # => reject immediately, same reasoning
return True # => no earlier queen attacks this square
def backtrack(row: int) -> bool: # => try to place a queen in every row, from `row` downward
if row == n: # => base case: every row has a queen -- a full solution was found
return True # => success propagates straight back up the recursion, no cols.pop() needed
for col in range(n): # => CHOICE POINT: try every column in this row
if is_safe(row, col): # => only attempt columns that don't conflict yet
cols.append(col) # => tentatively place the queen
if backtrack(row + 1): # => recurse to the next row
return True # => the whole rest of the board solved -- propagate success up
cols.pop() # => BACKTRACK: that column led nowhere, undo it and try the next column
return False # => no column in this row works given the current partial placement
return cols if backtrack(0) else None # => cols is fully built only if backtrack(0) succeeded
def no_two_queens_attack(cols: list[int]) -> bool: # => independent checker, used only for verification
for r1 in range(len(cols)): # => compare every pair of placed queens
for r2 in range(r1 + 1, len(cols)): # => r2 > r1 -- each pair checked exactly once, not twice
if cols[r1] == cols[r2]: # => same column
return False # => a violation was found -- no need to check any remaining pairs
if abs(cols[r1] - cols[r2]) == abs(r1 - r2): # => same diagonal
return False # => a violation was found -- no need to check any remaining pairs
return True # => every pair is safe
solution = solve_n_queens(8) # => the classic 8-queens problem
assert solution is not None # => narrow away None -- 8-queens always has a solution, matching test_example.py
print(solution) # => one valid arrangement (the specific columns depend on search order, but it is safe)
# => Output: [0, 4, 7, 5, 2, 6, 1, 3]
print(no_two_queens_attack(solution)) # => independently confirms no two queens attack each other
# => Output: TrueRun
python3 example.pyOutput
[0, 4, 7, 5, 2, 6, 1, 3]
Truetest_example.py
"""Example 37: pytest verification for Backtracking N-Queens."""
from example import no_two_queens_attack, solve_n_queens
def test_eight_queens_solution_has_no_attacking_pair() -> None:
solution = solve_n_queens(8) # => same size as the module-level demo
assert solution is not None # => a solution must exist for n=8
assert len(solution) == 8 # => one queen per row
assert no_two_queens_attack(solution) # => the independent checker must confirm safety
def test_four_queens_also_solves_safely() -> None:
solution = solve_n_queens(4) # => a smaller board, still solvable
assert solution is not None
assert no_two_queens_attack(solution)
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: cols.pop() inside backtrack() is the entire backtrack mechanism -- one line that
undoes a bad choice and lets the loop try the next candidate column, exactly the same shape as Example
36's continue.
Why it matters: this recursive "try, recurse, undo-and-retry" shape is reusable across wildly
different problems -- map coloring, Sudoku, and type inference later in this topic all reuse this exact
skeleton with only is_safe() changing. Solving 8-Queens by brute force would need to check all 8^8 (over 16 million) placements; backtracking's is_safe() check prunes an entire branch the instant a conflict appears, which is why the search finishes instantly instead of exhaustively enumerating every possibility.
Example 38: Constraint Map Coloring
ex-38 · exercises co-15
Map coloring declares "no two adjacent regions share a color" as data, and a backtrack() solver finds
an assignment satisfying it -- with no graph-coloring algorithm hand-written for this specific map.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
W["west"]:::blue ---|"must differ"| C["central"]:::orange
C ---|"must differ"| E["east"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 38: Constraint Map Coloring."""
Region = str # => a type alias -- purely for readability, region names are just strings
Color = str # => a type alias -- purely for readability, color names are just strings
adjacency: dict[Region, list[Region]] = { # => DECLARE the constraint: which regions must differ in color
"west": ["central"], # => west is adjacent to central only
"central": ["west", "east"], # => central is adjacent to both neighbors
"east": ["central"], # => east is adjacent to central only
} # => nothing here says HOW to search -- just which pairs may never share a color
colors: list[Color] = ["red", "green", "blue"] # => the available palette (3-coloring)
def solve_coloring(adj: dict[Region, list[Region]], palette: list[Color]) -> dict[Region, Color] | None: # => the GENERIC part: works for any adjacency map, any palette
regions = list(adj.keys()) # => a fixed order to assign regions in
assignment: dict[Region, Color] = {} # => the partial (then full) solution being built
def backtrack(index: int) -> bool: # => try to color every region from `index` onward
if index == len(regions): # => base case: every region has a color -- solved
return True # => the assignment dict already holds a complete, valid coloring
region = regions[index] # => the region we're choosing a color for right now
for color in palette: # => CHOICE POINT: try every color in the palette
if all(assignment.get(neighbor) != color for neighbor in adj[region]): # => check the constraint
assignment[region] = color # => tentatively assign
if backtrack(index + 1): # => recurse to the next region
return True # => success propagates straight back up -- no del assignment[region] needed
del assignment[region] # => BACKTRACK: undo this color, try the next one
return False # => no color in the palette works given the current partial assignment
return dict(assignment) if backtrack(0) else None # => copy out only on success
result = solve_coloring(adjacency, colors) # => run the solver
assert result is not None # => narrow away None -- this adjacency/palette pair always has a valid coloring
print(result) # => west and east may share a color; central must differ from both
# => Output: {'west': 'red', 'central': 'green', 'east': 'red'}
print(all(result[a] != result[b] for a, neighbors in adjacency.items() for b in neighbors)) # => verify
# => Output: TrueRun
python3 example.pyOutput
{'west': 'red', 'central': 'green', 'east': 'red'}
Truetest_example.py
"""Example 38: pytest verification for Constraint Map Coloring."""
from example import solve_coloring
def test_no_adjacent_regions_share_a_color() -> None:
adjacency = {"west": ["central"], "central": ["west", "east"], "east": ["central"]}
colors = ["red", "green", "blue"]
result = solve_coloring(adjacency, colors)
assert result is not None # => a valid 3-coloring must exist for this simple adjacency graph
for region, neighbors in adjacency.items(): # => check every declared constraint holds
for neighbor in neighbors:
assert result[region] != result[neighbor] # => the core map-coloring constraint
def test_two_colors_are_insufficient_for_a_triangle() -> None:
triangle = {"a": ["b", "c"], "b": ["a", "c"], "c": ["a", "b"]} # => every region touches both others
assert solve_coloring(triangle, ["red", "green"]) is None # => a triangle needs at least 3 colors
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: adjacency and colors are the entire problem statement; solve_coloring() is a
generic backtracking search that never mentions "west," "central," or "east" by name.
Why it matters: separating "what must be true" (the adjacency constraint) from "how to search for
it" (the backtracking loop) means the exact same solve_coloring() function reuses unchanged for a
triangle graph in the tests, or for a completely different map. A hand-written coloring algorithm specific to this three-region map would need to be rewritten for a different map's shape; solve_coloring() never mentions a region by name, so the identical function handles the test suite's triangle graph with zero code changes.
Example 39: Constraint Mini Sudoku (4x4)
ex-39 · exercises co-15
A 4x4 Sudoku solved by declaring row, column, and 2x2-box constraints and backtracking over empty cells -- reusing the identical choice-point/backtrack shape from Examples 37 and 38.
example.py
"""Example 39: Constraint Mini Sudoku (4x4)."""
Board = list[list[int]] # => a 4x4 grid; 0 marks an empty cell
# => DECLARE the puzzle: 0 = empty, otherwise a fixed clue -- one valid 4x4 sudoku with three clues
puzzle: Board = [ # => four rows, four clues total -- the rest is left for the solver to fill in
[1, 0, 0, 0], # => row 0: a 1 fixed in column 0
[0, 0, 1, 0], # => row 1: a 1 fixed in column 2
[0, 1, 0, 0], # => row 2: a 1 fixed in column 1
[0, 0, 0, 1], # => row 3: a 1 fixed in column 3
] # => closes the puzzle's initial 4x4 grid
def box_id(row: int, col: int) -> tuple[int, int]: # => which 2x2 box a cell belongs to
return (row // 2, col // 2) # => integer division groups rows/cols into 2x2 quadrants
def is_valid(board: Board, row: int, col: int, value: int) -> bool: # => the three sudoku constraints
if any(board[row][c] == value for c in range(4)): # => row constraint: no repeat in the row
return False # => reject immediately -- no need to check column or box constraints
if any(board[r][col] == value for r in range(4)): # => column constraint: no repeat in the column
return False # => reject immediately -- no need to check the box constraint
br, bc = box_id(row, col) # => box constraint: no repeat in the same 2x2 box
for r in range(br * 2, br * 2 + 2): # => the two rows of this cell's own 2x2 box
for c in range(bc * 2, bc * 2 + 2): # => the two columns of this cell's own 2x2 box
if board[r][c] == value: # => a same-box cell already holds this value
return False # => reject -- the value would appear twice in the same box
return True # => all three constraints satisfied
def solve(board: Board) -> Board | None: # => backtracking search over empty cells
for row in range(4): # => find the first empty cell, in reading order
for col in range(4): # => scan every column of this row before moving to the next row
if board[row][col] == 0: # => this is the next cell to fill
for value in range(1, 5): # => CHOICE POINT: try every candidate digit 1-4
if is_valid(board, row, col, value): # => only try digits that satisfy all constraints
board[row][col] = value # => tentatively place it
if solve(board): # => recurse into the rest of the board
return board # => success propagates straight up -- no undo needed here
board[row][col] = 0 # => BACKTRACK: undo, try the next candidate digit
return None # => no digit worked for this cell given the current partial board
return board # => no empty cells remain -- fully solved
solution = solve([row[:] for row in puzzle]) # => solve a COPY so the original `puzzle` stays untouched
assert solution is not None # => narrow away None -- this puzzle's three clues always admit a solution
print(solution) # => a fully filled, constraint-satisfying 4x4 grid
# => Output: [[1, 2, 3, 4], [3, 4, 1, 2], [2, 1, 4, 3], [4, 3, 2, 1]]
rows_ok = all(sorted(row) == [1, 2, 3, 4] for row in solution) # => every row has 1-4 exactly once
cols_ok = all(sorted(solution[r][c] for r in range(4)) == [1, 2, 3, 4] for c in range(4)) # => every column
print(rows_ok and cols_ok) # => independently confirms rows and columns are valid
# => Output: TrueRun
python3 example.pyOutput
[[1, 2, 3, 4], [3, 4, 1, 2], [2, 1, 4, 3], [4, 3, 2, 1]]
Truetest_example.py
"""Example 39: pytest verification for Constraint Mini Sudoku (4x4)."""
from example import box_id, solve
def test_solved_board_has_valid_rows_columns_and_boxes() -> None:
puzzle = [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]] # => same puzzle as the demo
solution = solve([row[:] for row in puzzle]) # => solve a defensive copy
assert solution is not None # => this puzzle is solvable
for row in solution: # => row constraint
assert sorted(row) == [1, 2, 3, 4]
for col in range(4): # => column constraint
assert sorted(solution[r][col] for r in range(4)) == [1, 2, 3, 4]
boxes: dict[tuple[int, int], list[int]] = {} # => box constraint
for r in range(4):
for c in range(4):
boxes.setdefault(box_id(r, c), []).append(solution[r][c])
for values in boxes.values():
assert sorted(values) == [1, 2, 3, 4]
def test_original_puzzle_is_not_mutated_by_solving_a_copy() -> None:
puzzle = [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]] # => fresh puzzle for this test
before = [row[:] for row in puzzle] # => snapshot before solving
solve([row[:] for row in puzzle]) # => solve a copy, discard the result
assert puzzle == before # => the original puzzle list is byte-identical to its snapshot
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: is_valid() checks all three declared constraints (row, column, box) before
solve() commits to a digit -- the exact same choice-point/backtrack skeleton as Examples 37 and 38,
just with a richer constraint check.
Why it matters: constraint programming's payoff compounds -- the same solve() shape now handles
three different problems (N-Queens, map coloring, Sudoku) with only the domain-specific validity check
changing each time. is_valid() here checks three constraints (row, column, box) instead of Example 38's one (adjacency), yet solve()'s own backtracking structure is unchanged from Example 38's, which is the concrete, measurable version of "the search skeleton generalizes."
Example 40: Event-Driven Loop
ex-40 · exercises co-16
A FIFO deque of events drained by an explicit while queue: loop, routed to handlers through a
dictionary -- the event loop pattern underlying most event-driven frameworks.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
Q["queue: [login/alice, login/bob, logout/alice]"]:::blue -->|popleft| H1["on_login(alice)"]:::orange
Q -->|popleft| H2["on_login(bob)"]:::orange
Q -->|popleft| H3["on_logout(alice)"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 40: Event-Driven Loop."""
from collections import deque # => deque gives O(1) popleft(), the FIFO queue's core operation
from collections.abc import Callable # => types every handler stored in the routing table below
from dataclasses import dataclass # => @dataclass auto-generates Event's __init__ from its fields
@dataclass # => auto-generates Event's __init__ from the two fields below
class Event: # => a plain data record describing what happened
kind: str # => which handler should process this event, e.g. "login"
payload: str # => the data the handler needs, e.g. a username
processed: list[str] = [] # => records the order events were actually handled in
def on_login(event: Event) -> None: # => handler for "login" events
processed.append(f"login:{event.payload}") # => the framework calls this -- it never calls the loop
def on_logout(event: Event) -> None: # => handler for "logout" events
processed.append(f"logout:{event.payload}") # => same shape as on_login, different kind and record
handlers: dict[str, Callable[[Event], None]] = {"login": on_login, "logout": on_logout} # => routing table
queue: deque[Event] = deque( # => a FIFO queue -- events wait here until the loop drains them
[ # => the events, already enqueued in the order they should be processed
Event("login", "alice"), # => first to arrive, first to be handled
Event("login", "bob"), # => second to arrive
Event("logout", "alice"), # => third to arrive
] # => closes the initial list of queued events
) # => closes the deque(...) constructor call
while queue: # => the event loop: keep draining until the queue is empty
event = queue.popleft() # => take the OLDEST event first -- FIFO order
handlers[event.kind](event) # => route it to its handler and run that handler NOW
# => the loop itself decides WHEN each handler runs -- the handler never calls back into the loop
print(processed) # => events must be processed in the exact order they were enqueued
# => Output: ['login:alice', 'login:bob', 'logout:alice']Run
python3 example.pyOutput
['login:alice', 'login:bob', 'logout:alice']test_example.py
"""Example 40: pytest verification for Event-Driven Loop."""
from collections import deque
from collections.abc import Callable
from example import Event
def test_events_processed_in_fifo_order() -> None:
seen: list[str] = [] # => local recorder, isolated from the module-level demo
handlers: dict[str, Callable[[Event], None]] = {
"login": lambda e: seen.append(f"login:{e.payload}"),
"logout": lambda e: seen.append(f"logout:{e.payload}"),
}
queue = deque([Event("login", "x"), Event("logout", "y"), Event("login", "z")])
while queue:
event = queue.popleft()
handlers[event.kind](event)
assert seen == ["login:x", "logout:y", "login:z"] # => exactly the enqueue order, unchanged
def test_named_handlers_produce_the_documented_module_level_trace() -> None:
from example import processed # => the module-level demo already ran when example.py was imported
assert processed == ["login:alice", "login:bob", "logout:alice"] # => matches example.py's own Output
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: handlers[event.kind](event) is the entire dispatch mechanism -- the while queue:
loop is the "engine" every event-driven framework hides behind an API, drained here in plain sight.
Why it matters: Example 16's Dispatcher.fire() triggered one handler on demand; this example makes
the underlying queue-draining loop visible, which is what a GUI toolkit or message broker runs
continuously behind the scenes. The while queue: loop here is not a toy simplification -- production event loops in async runtimes and UI frameworks follow the identical pop-dispatch-repeat shape, just with a queue fed by I/O completions or user input instead of a hand-built list of three events.
Example 41: Reactive Derived Value
ex-41 · exercises co-17
Computed subscribes to every Signal it depends on and recomputes itself automatically whenever any
of them change -- no manual "update c" call needed anywhere.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["Signal a"]:::blue -->|"on_change"| C["Computed c = a + b"]:::teal
B["Signal b"]:::orange -->|"on_change"| C
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 41: Reactive Derived Value."""
from collections.abc import Callable # => types every no-argument callback stored below
class Signal: # => a reactive source value that notifies dependents automatically
def __init__(self, initial: int) -> None: # => constructor seeds the starting value
self._value = initial # => the current value, hidden behind get()/set() below
self._on_change: list[Callable[[], None]] = [] # => callbacks to run whenever this signal changes
def get(self) -> int: # => read the current value
return self._value # => a plain read -- getting never triggers propagation
def set(self, value: int) -> None: # => write a new value and PUSH the change to every dependent
self._value = value # => update the internal box first
for callback in self._on_change: # => automatically notify -- no dependent has to poll
callback() # => runs the dependent's recompute hook synchronously, right here
def on_change(self, callback: Callable[[], None]) -> None: # => register a dependent's recompute hook
self._on_change.append(callback) # => append only -- does NOT call callback with the current value
class Computed: # => a derived signal: recomputes automatically whenever a source changes
def __init__(self, compute: Callable[[], int], *sources: Signal) -> None: # => wires up every source
self._compute = compute # => the formula, e.g. "a.get() + b.get()"
self.value = compute() # => compute once immediately, so `c` is correct before any update
for source in sources: # => subscribe to EVERY source this computed value depends on
source.on_change(self._recompute) # => wire automatic propagation
def _recompute(self) -> None: # => runs automatically whenever ANY source signal changes
self.value = self._compute() # => re-run the formula and refresh the cached value
a = Signal(1) # => source signal a
b = Signal(2) # => source signal b
c = Computed(lambda: a.get() + b.get(), a, b) # => c = a + b, kept up to date automatically
print(c.value) # => 1 + 2, computed at construction time
# => Output: 3
a.set(10) # => changing a automatically triggers c's recompute -- no manual "update c" call needed
print(c.value) # => 10 + 2
# => Output: 12
b.set(20) # => changing b ALSO automatically triggers c's recompute
print(c.value) # => 10 + 20
# => Output: 30Run
python3 example.pyOutput
3
12
30test_example.py
"""Example 41: pytest verification for Reactive Derived Value."""
from example import Computed, Signal
def test_computed_value_after_two_updates() -> None:
a = Signal(1) # => fresh signals, isolated from the module-level demo
b = Signal(2)
c = Computed(lambda: a.get() + b.get(), a, b)
assert c.value == 3 # => 1 + 2 at construction
a.set(10) # => update source a
assert c.value == 12 # => c recomputed automatically: 10 + 2
b.set(20) # => update source b too
assert c.value == 30 # => c recomputed again: 10 + 20
def test_computed_never_needs_a_manual_recompute_call() -> None:
a = Signal(0) # => a fresh independent pair of signals
b = Signal(0)
c = Computed(lambda: a.get() * b.get(), a, b)
a.set(5)
b.set(4)
assert c.value == 20 # => 5 * 4, with no explicit `c.recompute()` call anywhere in this test
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: Computed.__init__ subscribes to every source via source.on_change(self._recompute)
-- from that point on, a.set(10) and b.set(20) both automatically refresh c.value with no
recompute() call anywhere in the calling code.
Why it matters: Example 18's dataflow Cell needed an explicit recompute() call; Computed closes
that gap entirely, which is exactly what "reactive" adds on top of plain dataflow -- automatic
propagation, not just an expressible dependency. Measured directly: Example 18 required one explicit b.recompute() call per stale read, a call a caller could easily forget; here, a.set(10) and b.set(20) both refresh c.value with zero calls beyond the set() itself, closing that exact forgetting-to-update risk.
Example 42: Reactive vs Manual Recompute
ex-42 · exercises co-08, co-17
Contrasts a ManualPair whose total silently goes stale if recompute_total() is forgotten against a
ReactivePair built on the same Signal primitive from Example 41, which can never go stale.
example.py
"""Example 42: Reactive vs Manual Recompute."""
from collections.abc import Callable # => types every no-argument callback stored below
class ManualPair: # => BEFORE: caller must REMEMBER to update the derived value by hand
def __init__(self, a: int, b: int) -> None: # => constructor seeds both inputs and the derived total
self.a = a # => plain field, no notification wiring at all
self.b = b # => plain field, no notification wiring at all
self.total = a + b # => computed once -- nothing keeps this in sync automatically
def set_a(self, value: int) -> None: # => updates a but does NOT touch total -- easy to forget
self.a = value # => total is now STALE until someone remembers to call recompute_total()
def recompute_total(self) -> None: # => the easy-to-forget manual step
self.total = self.a + self.b # => must be called explicitly -- nothing calls it automatically
class Signal: # => AFTER: the same minimal reactive primitive as example 41
def __init__(self, initial: int) -> None: # => constructor seeds the starting value
self._value = initial # => the current value, hidden behind get()/set() below
self._on_change: list[Callable[[], None]] = [] # => callbacks to run whenever this signal changes
def get(self) -> int: # => read the current value
return self._value # => a plain read -- getting never triggers propagation
def set(self, value: int) -> None: # => setting AUTOMATICALLY notifies every dependent
self._value = value # => update the internal box first
for callback in self._on_change: # => automatically notify -- no dependent has to poll
callback() # => runs the dependent's recompute hook synchronously, right here
def on_change(self, callback: Callable[[], None]) -> None: # => register a dependent's recompute hook
self._on_change.append(callback) # => append only -- does NOT call callback with the current value
class ReactivePair: # => wires a and b so total NEVER goes stale
def __init__(self, a: int, b: int) -> None: # => constructor wraps both inputs as Signals
self.a = Signal(a) # => a is now reactive, not a plain field
self.b = Signal(b) # => b is now reactive, not a plain field
self.total = self.a.get() + self.b.get() # => initial value
self.a.on_change(self._recompute) # => subscribe -- total tracks a automatically
self.b.on_change(self._recompute) # => subscribe -- total tracks b automatically
def _recompute(self) -> None: # => runs automatically whenever a or b changes
self.total = self.a.get() + self.b.get() # => the SAME formula as ManualPair, but never forgotten
manual = ManualPair(1, 2) # => BEFORE
manual.set_a(10) # => forgot to call recompute_total() -- a realistic mistake
print(manual.total) # => STALE: still reflects the OLD a, not the new one
# => Output: 3
reactive = ReactivePair(1, 2) # => AFTER
reactive.a.set(10) # => the equivalent update, via the reactive API
print(reactive.total) # => automatically current: 10 + 2
# => Output: 12Run
python3 example.pyOutput
3
12test_example.py
"""Example 42: pytest verification for Reactive vs Manual Recompute."""
from example import ManualPair, ReactivePair
def test_manual_pair_goes_stale_if_recompute_is_forgotten() -> None:
pair = ManualPair(1, 2) # => fresh instance
pair.set_a(10) # => update a, but never call recompute_total()
assert pair.total == 3 # => STILL the old total -- this is the bug the reactive version prevents
def test_manual_pair_is_correct_only_after_an_explicit_recompute() -> None:
pair = ManualPair(1, 2) # => fresh instance
pair.set_a(10) # => update a
pair.recompute_total() # => remember to call it this time
assert pair.total == 12 # => now correct, but only because of the explicit call
def test_reactive_pair_is_always_consistent_automatically() -> None:
pair = ReactivePair(1, 2) # => fresh instance
pair.a.set(10) # => the same conceptual update, no manual recompute anywhere
assert pair.total == 12 # => correct immediately -- reactive propagation is automatic
# => Run: pytest -- Output: 3 passedVerify
pytest -qOutput
3 passedKey takeaway: manual.total stays at 3 after set_a(10) -- a genuine, reproducible bug from a
forgotten manual step; reactive.total becomes 12 automatically, from the identical conceptual update.
Why it matters: this is co-17's real payoff made visible as a bug you can watch happen -- reactive
propagation doesn't just save typing, it eliminates an entire class of "I changed A but forgot to update
the thing that depends on A" defects. The bug in ManualPair is not hypothetical: test_manual_pair_goes_stale_if_recompute_is_forgotten() reproduces it directly, and the identical mistake -- forgetting one call site among many -- gets easier to make, not harder, as a codebase grows past a handful of derived fields.
Example 43: Dataflow Topological Execute
ex-43 · exercises co-18
A dependency graph of formulas is executed strictly in topological order -- by the time any node runs, every node it depends on has already been computed.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["a (no deps)"]:::blue --> B["b = a + 1"]:::orange
A --> C["c = a + b"]:::teal
B --> C
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 43: Dataflow Topological Execute."""
from collections.abc import Callable # => types every node's compute formula stored below
Node = str # => a type alias -- node names are just strings
graph: dict[Node, list[Node]] = { # => DAG of value dependencies: node -> nodes it depends on
"c": ["a", "b"], # => c depends on a and b
"b": ["a"], # => b depends on a
"a": [], # => a has no dependencies -- a source node
} # => closes the dependency graph declaration
formulas: dict[Node, Callable[[dict[Node, int]], int]] = { # => how to compute each node, given results so far
"a": lambda results: 1, # => a is a constant
"b": lambda results: results["a"] + 1, # => b = a + 1
"c": lambda results: results["a"] + results["b"], # => c = a + b
} # => closes the per-node formula table
def topological_order(deps: dict[Node, list[Node]]) -> list[Node]: # => order respecting every dependency
visited: set[Node] = set() # => nodes already placed in the order
order: list[Node] = [] # => the order being built
def visit(node: Node) -> None: # => depth-first visit: dependencies before the node itself
if node in visited: # => already placed -- nothing to do
return # => stops the recursion for this branch -- a node is never appended twice
visited.add(node) # => mark BEFORE recursing to guard against revisiting a node mid-traversal
for dep in deps[node]: # => every dependency must appear in the order first
visit(dep) # => recurse into the dependency
order.append(node) # => only append AFTER all dependencies are already in `order`
for node in deps: # => make sure every node gets visited, regardless of starting point
visit(node) # => already-visited nodes short-circuit immediately via the check above
return order # => a valid topological order: every dependency precedes its dependents
order = topological_order(graph) # => compute the execution order
print(order) # => a must come before b and c; b must come before c
# => Output: ['a', 'b', 'c']
results: dict[Node, int] = {} # => accumulate computed values as we execute in order
for node in order: # => execute strictly in topological order
results[node] = formulas[node](results) # => by the time we reach a node, its deps are already computed
print(results) # => a=1, b=1+1=2, c=1+2=3
# => Output: {'a': 1, 'b': 2, 'c': 3}Run
python3 example.pyOutput
['a', 'b', 'c']
{'a': 1, 'b': 2, 'c': 3}test_example.py
"""Example 43: pytest verification for Dataflow Topological Execute."""
from collections.abc import Callable
from example import topological_order
def test_order_respects_every_dependency() -> None:
graph = {"c": ["a", "b"], "b": ["a"], "a": []} # => same graph as the module-level demo
order = topological_order(graph)
assert order.index("a") < order.index("b") # => a must precede b
assert order.index("a") < order.index("c") # => a must precede c
assert order.index("b") < order.index("c") # => b must precede c
def test_result_matches_the_documented_formulas() -> None:
graph = {"c": ["a", "b"], "b": ["a"], "a": []}
formulas: dict[str, Callable[[dict[str, int]], int]] = {
"a": lambda r: 1,
"b": lambda r: r["a"] + 1,
"c": lambda r: r["a"] + r["b"],
}
order = topological_order(graph)
results: dict[str, int] = {}
for node in order:
results[node] = formulas[node](results)
assert results == {"a": 1, "b": 2, "c": 3} # => matches example.py's own Output exactly
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: topological_order() produces ['a', 'b', 'c'] purely from the declared edges --
results[node] = formulas[node](results) never needs to check "is this dependency ready yet" because
the order already guarantees it.
Why it matters: once a computation is expressed as a dependency graph rather than a fixed sequence
of steps, a scheduler can find independent nodes automatically -- Example 63 later batches this exact
graph shape into parallel-ready waves. A hand-ordered sequence of assignments -- compute a, then b, then c -- would need to be re-checked by hand every time a new formula is added; topological_order() derives a valid execution order directly from the declared graph edges, so the ordering can never drift out of sync with the dependencies.
Example 44: Generator Pull Pipeline
ex-44 · exercises co-18
A three-stage source -> gen_map -> gen_filter pipeline stays lazy end to end -- take(pipeline, 3)
pulls exactly as many source values as needed, and not one more.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph LR
T["take(pipeline, 3)"]:::purple -->|pulls| F["gen_filter (even)"]:::teal
F -->|pulls| M["gen_map (square)"]:::orange
M -->|pulls| S["source (1, 2, 3, ...)"]:::blue
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 44: Generator Pull Pipeline."""
from collections.abc import Callable, Iterator # => every function below is typed as a lazy, pull-based Iterator
computed_log: list[int] = [] # => records every value the source generator actually produced
def source() -> Iterator[int]: # => an "infinite" source -- would hang if fully consumed eagerly
n = 0 # => starts at 0, incremented once per pull
while True: # => never terminates on its own -- laziness is the only thing that makes this safe
n += 1 # => the next candidate value
computed_log.append(n) # => record that this value was actually generated (proves pull, not push)
yield n # => PULL-based: this line only runs when something asks the generator for its next value
def gen_map(it: Iterator[int], fn: Callable[[int], int]) -> Iterator[int]: # => lazy map: transforms values ONE AT A TIME, on demand
for value in it: # => pulling from `it` only happens as this generator itself is pulled from
yield fn(value) # => nothing is computed until a consumer asks for the next item
def gen_filter(it: Iterator[int], predicate: Callable[[int], bool]) -> Iterator[int]: # => lazy filter: same pull-based contract
for value in it: # => each pull here triggers exactly one pull upstream
if predicate(value): # => only values passing the predicate are ever yielded downstream
yield value # => only yield values that pass the predicate
def take(it: Iterator[int], n: int) -> list[int]: # => the ONLY thing that actually drives the pipeline
result: list[int] = [] # => the concrete list being built, one pull at a time
for value in it: # => pulling n times cascades back through filter -> map -> source
result.append(value) # => record this match before checking whether we have enough yet
if len(result) == n: # => stop pulling the instant we have enough -- laziness in action
break # => no further pulls happen -- the upstream generators simply stay paused
return result # => exactly n items, and not one pull more than strictly needed to produce them
pipeline = gen_filter(gen_map(source(), lambda n: n * n), lambda n: n % 2 == 0) # => squares, then evens only
result = take(pipeline, 3) # => pull exactly 3 matching items -- nothing more
print(result) # => squares of 1..: 1,4,9,16,25,36,... ; even ones in order: 4, 16, 36
# => Output: [4, 16, 36]
print(len(computed_log)) # => the source only ran as many times as strictly needed to produce 3 matches
# => Output: 6Run
python3 example.pyOutput
[4, 16, 36]
6test_example.py
"""Example 44: pytest verification for Generator Pull Pipeline."""
from example import gen_filter, gen_map, source, take
def test_first_three_even_squares() -> None:
pipeline = gen_filter(gen_map(source(), lambda n: n * n), lambda n: n % 2 == 0)
assert take(pipeline, 3) == [4, 16, 36] # => same result as example.py's own Output
def test_pipeline_only_pulls_as_many_source_values_as_strictly_needed() -> None:
seen: list[int] = [] # => a fresh local counter, isolated from the module-level demo's `computed_log`
def counting_source():
n = 0
while True:
n += 1
seen.append(n)
yield n
pipeline = gen_filter(gen_map(counting_source(), lambda n: n * n), lambda n: n % 2 == 0)
take(pipeline, 1) # => only ask for ONE matching item
assert seen == [1, 2] # => n=1 (square 1, odd, rejected), n=2 (square 4, even, accepted) -- then stop
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: computed_log has exactly 6 entries after pulling 3 matches -- the source, an
infinite loop that would hang under eager evaluation, only ever advances as far as take() actually
pulls it.
Why it matters: this is co-10's expression-vs-statement idea taken to its logical extreme -- nothing
in this pipeline runs until something asks for a value, which is what lets an "infinite" source coexist
safely with a bounded consumer. An eager version of this same pipeline -- building the full mapped-and-filtered list before taking the first three -- would never terminate, because source() never stops on its own; the lazy generator chain instead computes exactly 6 values, measured directly via computed_log, to answer a request for 3.
Example 45: Inversion of Control
ex-45 · exercises co-16
Contrasts calling a function directly (your code drives the loop) against registering a handler with a
framework and letting framework.run() drive the identical loop instead.
example.py
"""Example 45: Inversion of Control."""
from collections.abc import Callable # => types the row-transforming function used in both styles
def render_report_you_call_library(rows: list[str], formatter: Callable[[str], str]) -> list[str]: # => plain function -- callers drive it directly
# => YOU-CALL-LIBRARY: your code drives the loop, YOU decide when to call the library function
return [formatter(row) for row in rows] # => your code is in charge of the control flow
class ReportFramework: # => FRAMEWORK-CALLS-YOU: the framework owns the loop, you just supply a hook
def __init__(self) -> None: # => constructor starts with no handler registered yet
self._on_row: Callable[[str], str] | None = None # => a slot for YOUR callback
def register(self, handler: Callable[[str], str]) -> None: # => you hand the framework your logic
self._on_row = handler # => the framework stores it, does not call it yet
def run(self, rows: list[str]) -> list[str]: # => the framework owns this loop entirely
assert self._on_row is not None, "must register a handler first" # => fail loudly if nothing was registered
return [self._on_row(row) for row in rows] # => the FRAMEWORK calls YOUR code, not the other way
rows = ["alice", "bob"] # => shared sample data
shout: Callable[[str], str] = lambda row: row.upper() # noqa: E731 # => the same transformation logic in both styles
you_call_result = render_report_you_call_library(rows, shout) # => your code drives the call
print(you_call_result) # => both styles must produce identical output
# => Output: ['ALICE', 'BOB']
framework = ReportFramework() # => construct the framework
framework.register(shout) # => hand it your handler -- inversion of control: framework decides when to call it
framework_result = framework.run(rows) # => the framework's run() loop is what actually invokes `shout`
print(framework_result) # => must be identical to the you-call-library result
# => Output: ['ALICE', 'BOB']
print(you_call_result == framework_result) # => confirms both control-flow styles agree
# => Output: TrueRun
python3 example.pyOutput
['ALICE', 'BOB']
['ALICE', 'BOB']
Truetest_example.py
"""Example 45: pytest verification for Inversion of Control."""
from collections.abc import Callable
from example import ReportFramework, render_report_you_call_library
def test_you_call_library_and_framework_calls_you_agree() -> None:
rows = ["x", "y", "z"] # => fresh sample, isolated from the module-level demo
handler: Callable[[str], str] = lambda row: row.upper() # noqa: E731
direct = render_report_you_call_library(rows, handler) # => your code drives the loop
framework = ReportFramework()
framework.register(handler) # => hand the SAME handler to the framework
inverted = framework.run(rows) # => the framework drives the loop this time
assert direct == inverted == ["X", "Y", "Z"] # => identical results, different control-flow owner
def test_framework_invokes_the_registered_handler_not_a_default() -> None:
framework = ReportFramework() # => fresh framework instance
calls: list[str] = [] # => records what the registered handler actually received
framework.register(lambda row: calls.append(row) or row) # => a handler with a visible side effect
framework.run(["p", "q"]) # => the framework is the one that calls it, per row
assert calls == ["p", "q"] # => confirms the framework actually invoked OUR handler, in order
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: render_report_you_call_library() and ReportFramework.run() produce identical
output from the identical shout handler -- the only difference is which side of the call owns the
loop.
Why it matters: this is "inversion of control" made concrete -- in event-driven code, the framework
decides when your handler runs, based on events it observes, and you trade away control of the timeline
in exchange for not writing the loop yourself. Both styles above produce the byte-identical ['ALICE', 'BOB'] from the same shout handler, so the only real difference inversion of control buys is who owns the loop -- a trade web frameworks, GUI toolkits, and test runners all make deliberately, not by accident.
Example 46: Declarative Config vs Setup
ex-46 · exercises co-08
Builds an identical Server object two ways: step-by-step mutation after construction versus one call
reading every field from a single declared spec dict.
example.py
"""Example 46: Declarative Config vs Setup."""
from dataclasses import dataclass, field # => @dataclass generates __init__; field() gives a fresh list
@dataclass # => auto-generates Server's __init__ from the three fields below
class Server: # => the object both styles below must end up constructing, identically
host: str = "localhost" # => default value, overridden by both build functions below
port: int = 8080 # => default value, overridden by both build functions below
routes: list[str] = field(default_factory=list[str]) # => default: a fresh empty list per instance
def build_via_imperative_setup() -> Server: # => HOW: step-by-step mutation after construction
server = Server() # => start from the defaults
server.host = "api.example.com" # => step 1: mutate host
server.port = 443 # => step 2: mutate port
server.routes.append("/health") # => step 3: mutate routes
server.routes.append("/users") # => step 4: mutate routes again
return server # => the fully-mutated object
DECLARED_SPEC: dict[str, object] = { # => WHAT: the desired final shape, stated as data up front
"host": "api.example.com", # => same value the imperative version reaches via mutation
"port": 443, # => same value the imperative version reaches via mutation
"routes": ["/health", "/users"], # => same value the imperative version reaches via two .append() calls
} # => closes the declared spec -- one value, no steps
def build_via_declared_spec(spec: dict[str, object]) -> Server: # => construct directly FROM the spec
return Server(host=str(spec["host"]), port=int(spec["port"]), routes=list(spec["routes"])) # => reads the whole desired shape from spec in one call # type: ignore[arg-type]
# => one call, reading the entire desired shape from a single declared value
imperative_server = build_via_imperative_setup() # => run the step-by-step version
declarative_server = build_via_declared_spec(DECLARED_SPEC) # => run the declared-spec version
print(imperative_server) # => both objects must be field-for-field equal
# => Output: Server(host='api.example.com', port=443, routes=['/health', '/users'])
print(imperative_server == declarative_server) # => dataclasses compare structurally by default
# => Output: TrueRun
python3 example.pyOutput
Server(host='api.example.com', port=443, routes=['/health', '/users'])
Truetest_example.py
"""Example 46: pytest verification for Declarative Config vs Setup."""
from example import Server, build_via_declared_spec, build_via_imperative_setup
def test_both_construction_styles_produce_an_equal_object() -> None:
imperative = build_via_imperative_setup() # => step-by-step mutation
declared = build_via_declared_spec({"host": "api.example.com", "port": 443, "routes": ["/health", "/users"]})
assert imperative == declared # => dataclass structural equality
def test_declared_spec_with_different_values_builds_a_different_server() -> None:
server = build_via_declared_spec({"host": "other.example.com", "port": 80, "routes": []})
assert server == Server(host="other.example.com", port=80, routes=[]) # => matches the given spec exactly
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: build_via_declared_spec() needs no intermediate assignments -- the entire desired
shape of Server is already sitting in DECLARED_SPEC before construction even begins.
Why it matters: a declared spec can be validated, diffed, or serialized as data before it ever
touches an object -- a sequence of imperative mutations offers none of those properties, because "the
desired shape" only ever exists as a trail of side effects. DECLARED_SPEC could be dumped as JSON, checked against a schema, or diffed against yesterday's version without constructing a single Server; build_via_imperative_setup()'s four mutation steps have no equivalent snapshot to inspect until every one of them has already run.
Example 47: Relational vs Nested-Loop Join
ex-47 · exercises co-19
A JOIN ... ON query and a hand-written nested loop compute the identical customer/order pairing --
one declares the relationship, the other spells out the matching mechanics.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
C["customers"]:::blue -->|"id = orders.customer_id"| J["JOIN"]:::teal
O["orders"]:::orange -->|"id = orders.customer_id"| J
J --> R["(name, item) pairs"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 47: Relational vs Nested-Loop Join."""
import sqlite3 # => the standard library's built-in SQL engine -- no external dependency needed
customers: list[tuple[int, str]] = [(1, "alice"), (2, "bob")] # => (customer_id, name)
orders: list[tuple[int, int, str]] = [(101, 1, "widget"), (102, 2, "gadget"), (103, 1, "gizmo")]
# => (order_id, customer_id, item)
def join_via_sql(customers: list[tuple[int, str]], orders: list[tuple[int, int, str]]) -> list[tuple[str, str]]: # => declarative leg
conn = sqlite3.connect(":memory:") # => declare tables, state the join, let SQLite compute it
conn.execute("CREATE TABLE customers (id INTEGER, name TEXT)") # => declares the shape, no data yet
conn.execute("CREATE TABLE orders (id INTEGER, customer_id INTEGER, item TEXT)") # => same, for orders
conn.executemany("INSERT INTO customers VALUES (?, ?)", customers) # => load every customer row
conn.executemany("INSERT INTO orders VALUES (?, ?, ?)", orders) # => load every order row
rows = conn.execute( # => the query IS the join -- no accumulator variable anywhere in this function
"SELECT customers.name, orders.item FROM customers JOIN orders ON customers.id = orders.customer_id ORDER BY orders.id"
# => JOIN...ON declares the relationship -- no explicit loop nesting anywhere in this code
).fetchall() # => the query planner decided HOW to match rows; this call only asks for the results
conn.close() # => release the in-memory connection
return rows # => list of (name, item) pairs
def join_via_nested_loop(customers: list[tuple[int, str]], orders: list[tuple[int, int, str]]) -> list[tuple[str, str]]: # => imperative leg
result: list[tuple[str, str]] = [] # => mutable accumulator
for _order_id, customer_id, item in orders: # => outer loop: every order, in insertion order
for cid, name in customers: # => inner loop: scan every customer looking for a match
if cid == customer_id: # => the join condition, written out explicitly as a comparison
result.append((name, item)) # => explicit accumulation, one matched pair at a time
break # => stop scanning customers once this order's match is found
return result # => the fully built accumulator
sql_result = join_via_sql(customers, orders) # => declarative version
loop_result = join_via_nested_loop(customers, orders) # => imperative version
print(sql_result) # => both must produce identical (name, item) pairs, in the same order
# => Output: [('alice', 'widget'), ('bob', 'gadget'), ('alice', 'gizmo')]
print(sql_result == loop_result) # => confirms the declarative and imperative joins agree
# => Output: TrueRun
python3 example.pyOutput
[('alice', 'widget'), ('bob', 'gadget'), ('alice', 'gizmo')]
Truetest_example.py
"""Example 47: pytest verification for Relational vs Nested-Loop Join."""
from example import join_via_nested_loop, join_via_sql
def test_sql_and_nested_loop_joins_produce_identical_rows() -> None:
customers = [(1, "alice"), (2, "bob")] # => same fixtures as the module-level demo
orders = [(101, 1, "widget"), (102, 2, "gadget"), (103, 1, "gizmo")]
assert join_via_sql(customers, orders) == join_via_nested_loop(customers, orders)
def test_an_order_with_no_matching_customer_is_dropped_by_both_joins() -> None:
customers = [(1, "alice")] # => only customer 1 exists
orders = [(101, 1, "widget"), (102, 99, "orphan")] # => order 102 references a nonexistent customer
sql_rows = join_via_sql(customers, orders) # => an INNER JOIN drops unmatched orders
loop_rows = join_via_nested_loop(customers, orders) # => the nested loop's `break` also drops it
assert sql_rows == loop_rows == [("alice", "widget")] # => both agree: the orphan order vanishes
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: JOIN orders ON customers.id = orders.customer_id states the relationship once; the
nested loop restates it as an explicit if cid == customer_id check inside two levels of iteration --
both produce the identical three-row result.
Why it matters: SQLite's query planner is free to choose an index, a hash join, or any other
strategy behind that one JOIN clause; the nested loop's performance is locked into exactly the loop
structure written on the page. With a thousand customers and a thousand orders, the nested loop's O(n*m) scan explores up to a million comparisons no matter what; SQLite can build an index on customers.id and turn the same declared JOIN into a lookup, without a single line of this file's Python changing.
Example 48: Pure Core, Imperative Shell
ex-48 · exercises co-11, co-25
compute_invoice_total() is a pure core tested with zero I/O; print_invoice() is a thin imperative
shell whose only job is to print the core's result.
example.py
"""Example 48: Pure Core, Imperative Shell."""
def compute_invoice_total(unit_price: int, quantity: int, discount_pct: int) -> int: # => the PURE CORE
subtotal = unit_price * quantity # => no I/O, no globals, only its own arguments
discount = subtotal * discount_pct // 100 # => pure arithmetic
return subtotal - discount # => deterministic: same inputs always produce the same output
# => this function can be tested with zero I/O, zero mocks, zero setup -- that is the whole point
def print_invoice(unit_price: int, quantity: int, discount_pct: int) -> None: # => the IMPERATIVE SHELL
total = compute_invoice_total(unit_price, quantity, discount_pct) # => delegate all logic to the core
print(f"Total: {total}") # => the ONLY line in this file that performs I/O -- the shell's whole job
# => the shell contains no business logic of its own -- it just wires the pure core to the outside world
print_invoice(1000, 3, 10) # => 1000*3=3000, 10% off = 300, total 2700
# => Output: Total: 2700Run
python3 example.pyOutput
Total: 2700test_example.py
"""Example 48: pytest verification for Pure Core, Imperative Shell."""
from example import compute_invoice_total
def test_core_is_tested_with_zero_io_and_zero_mocks() -> None:
# => this test never touches print(), a file, or a network call -- proves the core needs no I/O
assert compute_invoice_total(1000, 3, 10) == 2700 # => 3000 - 300
def test_core_is_deterministic_across_repeated_calls() -> None:
first = compute_invoice_total(500, 2, 20) # => call #1
second = compute_invoice_total(500, 2, 20) # => call #2, identical arguments
assert first == second == 800 # => 1000 - 200, same both times -- no hidden state anywhere
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: test_core_is_tested_with_zero_io_and_zero_mocks() genuinely never touches print()
-- the entire business logic (compute_invoice_total) is testable without a single mock.
Why it matters: this is co-25's boundary discipline in its cleanest form -- print_invoice() is
purely plumbing, and everything worth getting right lives in a function that has no side effects at all. test_core_is_tested_with_zero_io_and_zero_mocks() runs in microseconds and needs no test fixture beyond three integers; a shell-heavy design that mixed the discount math into print_invoice() itself would force every test of that math to also capture and parse printed output.
Example 49: Multi-Paradigm Boundary
ex-49 · exercises co-20, co-25
A pure functional pipeline hands its result -- an immutable tuple -- to an OO InventoryService across
a clean boundary; crossing it never mutates the tuple.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
F["functional_pipeline(raw_prices)"]:::blue -->|"immutable tuple"| B{"boundary"}:::teal
B --> S["InventoryService.record_batch()"]:::orange
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 49: Multi-Paradigm Boundary."""
from dataclasses import dataclass
def functional_pipeline(raw_prices: tuple[int, ...]) -> tuple[int, ...]: # => a pure functional pipeline
discounted = tuple(p - (p * 10 // 100) for p in raw_prices) # => map: apply a 10% discount
return tuple(p for p in discounted if p > 0) # => filter: drop non-positive prices
# => every step returns a NEW immutable tuple -- nothing here is ever mutated in place
@dataclass # => an OO service the pipeline hands its result to, across a clean boundary
class InventoryService:
accepted_prices: list[int]
def record_batch(self, prices: tuple[int, ...]) -> None: # => the ONLY place mutation happens
self.accepted_prices.extend(prices) # => OO-style in-place mutation, but confined to this class
raw_prices = (100, 50, 5, 200) # => an immutable tuple -- the functional side's input
cleaned = functional_pipeline(raw_prices) # => pure functional processing, no mutation anywhere yet
print(cleaned) # => 100->90, 50->45, 5->5 (5*10//100=0 discount, still positive), 200->180; nothing dropped
# => Output: (90, 45, 5, 180)
service = InventoryService(accepted_prices=[]) # => the OO side of the boundary
service.record_batch(cleaned) # => the boundary: an immutable tuple crosses into a mutable OO object
print(service.accepted_prices) # => confirms the OO side received exactly the functional side's output
# => Output: [90, 45, 5, 180]
print(cleaned) # => the tuple itself is STILL untouched -- crossing the boundary never mutated it
# => Output: (90, 45, 5, 180)Run
python3 example.pyOutput
(90, 45, 5, 180)
[90, 45, 5, 180]
(90, 45, 5, 180)test_example.py
"""Example 49: pytest verification for Multi-Paradigm Boundary."""
from example import InventoryService, functional_pipeline
def test_boundary_passes_only_immutable_data() -> None:
cleaned = functional_pipeline((100, 50, 5, 200)) # => run the pure side
assert isinstance(cleaned, tuple) # => the boundary value itself is immutable
service = InventoryService(accepted_prices=[]) # => fresh OO service
service.record_batch(cleaned) # => cross the boundary
assert service.accepted_prices == [90, 45, 5, 180] # => the OO side received the pipeline's output
assert cleaned == (90, 45, 5, 180) # => and the tuple itself is provably unchanged after crossing
def test_functional_side_never_mutates_its_own_input_tuple() -> None:
raw = (10, 20) # => a small immutable input
functional_pipeline(raw) # => call once, discard the result -- only checking for mutation
assert raw == (10, 20) # => tuples cannot be mutated in place at all, but this documents the contract
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: cleaned is still (90, 45, 5, 180) after service.record_batch(cleaned) runs --
the boundary between the functional pipeline and the OO service passes an immutable value, so the OO
side's own mutation (extend) can never reach back into the functional side.
Why it matters: this is a positive example of co-25's discipline -- one paradigm per boundary, with
the immutability of the crossed value enforcing that the OO side's mutation stays contained on its own
side. Contrast this with Example 50's MutableBucket, where the same kind of boundary crossing corrupts the caller's own reference; here cleaned stays (90, 45, 5, 180) after crossing into InventoryService, because a tuple gives record_batch() nothing it could mutate even if it tried.
Example 50: Paradigm Soup Anti-Pattern
ex-50 · exercises co-25
Reproduces the failure mode Example 49 avoided: a "functional-looking" chain of two functions secretly
mutates a shared MutableBucket, corrupting the caller's own reference.
example.py
"""Example 50: Paradigm Soup Anti-Pattern."""
class MutableBucket: # => an OO object with mutable state, threaded through a nominally "functional" pipeline
def __init__(self, items: list[int]) -> None:
self.items = items # => a MUTABLE list, not a tuple -- this is the seed of the bug
def add_bonus_functional_looking(bucket: MutableBucket) -> MutableBucket: # => LOOKS like a pure map step...
bucket.items.append(999) # => ...but secretly MUTATES the shared list in place -- paradigm soup
return bucket # => returning the SAME mutated object, not a new one, is the tell
def scale_functional_looking(bucket: MutableBucket) -> MutableBucket: # => a second "map step"
for i in range(len(bucket.items)): # => also mutates in place, hidden behind a function-call facade
bucket.items[i] *= 2 # => in-place scaling
return bucket # => same object identity as the input -- no new value was actually created
original = MutableBucket([1, 2, 3]) # => construct the shared mutable object once
step1 = add_bonus_functional_looking(original) # => "looks like" pipe(original, add_bonus)
step2 = scale_functional_looking(step1) # => "looks like" pipe(step1, scale) -- chained, functional style
print(step2.items) # => the visible chained result
# => Output: [2, 4, 6, 1998]
print(original.items) # => THE BUG: `original` was aliased and mutated by every "pipeline" step
# => Output: [2, 4, 6, 1998]
print(original is step1 is step2) # => all three names point at the SAME object -- no new values anywhere
# => Output: TrueRun
python3 example.pyOutput
[2, 4, 6, 1998]
[2, 4, 6, 1998]
Truetest_example.py
"""Example 50: pytest verification for Paradigm Soup Anti-Pattern."""
from example import MutableBucket, add_bonus_functional_looking, scale_functional_looking
def test_aliasing_bug_reproduces_original_is_silently_mutated() -> None:
original = MutableBucket([1, 2, 3]) # => fresh bucket, isolated from the module-level demo
result = scale_functional_looking(add_bonus_functional_looking(original)) # => a "functional-looking" chain
assert result.items == [2, 4, 6, 1998] # => the chained result
assert original.items == [2, 4, 6, 1998] # => THE BUG: the "original" reference was mutated too
assert original is result # => same object identity -- no new value was ever produced
def test_a_true_immutable_pipeline_would_not_have_this_bug() -> None:
original: tuple[int, ...] = (1, 2, 3) # => the honest functional fix: use an immutable tuple instead
bonus = original + (999,) # => a genuinely NEW tuple, original untouched
scaled = tuple(n * 2 for n in bonus) # => another genuinely NEW tuple
assert original == (1, 2, 3) # => the true-functional version never mutates the original at all
assert scaled == (2, 4, 6, 1998) # => and still produces the identical final values
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: original is step1 is step2 is True -- the "pipeline" never produced a single new
value; every step silently mutated the same shared object, so the caller's own original reference was
corrupted without their knowledge.
Why it matters: this is co-25's warning label made concrete -- code that merely LOOKS functional
(chained calls, no visible loop) can still collect every cost of shared mutable state if the guarantee
"this value can't be mutated" is only cosmetic. The fix in test_a_true_immutable_pipeline_would_not_have_this_bug() needs only one change -- swap MutableBucket's list for a tuple, the same substitution Example 49 makes from the start -- to eliminate the aliasing bug entirely, without touching either function's own logic.
Example 51: Logic vs Imperative Reachability
ex-51 · exercises co-01, co-13
A cyclic graph's reachable set is computed two ways: a logic-flavored fixed-point inference and an explicit BFS -- both must correctly include the cycle's own origin node.
example.py
"""Example 51: Logic vs Imperative Reachability."""
from collections import deque # => deque gives O(1) popleft(), the BFS queue's core operation
edges: dict[str, list[str]] = { # => a directed graph, shared by both approaches below
"a": ["b", "c"], # => a points to b and c
"b": ["d"], # => b points to d
"c": [], # => c has no outgoing edges
"d": ["a"], # => a cycle back to "a" -- both approaches must handle this without looping forever
"e": [], # => an unreachable, isolated node
} # => closes the shared graph declaration
def reachable_via_inference(start: str, edges: dict[str, list[str]]) -> set[str]: # => LOGIC-flavored
# => rule: reachable(X, Y) :- edge(X, Y). reachable(X, Z) :- edge(X, Y), reachable(Y, Z).
known: set[str] = set() # => the set of facts inferred so far (a fixed-point computation)
frontier = set(edges.get(start, [])) # => seed with everything directly reachable via one edge
while frontier - known: # => keep inferring new facts until nothing new can be derived (fixed point)
newly_known = frontier - known # => facts inferred in THIS round that weren't already known
known |= newly_known # => add them to the known set
frontier = known | {y for x in newly_known for y in edges.get(x, [])} # => derive one more hop
return known # => the full set of inferred "reachable" facts
def reachable_via_bfs(start: str, edges: dict[str, list[str]]) -> set[str]: # => IMPERATIVE: explicit BFS
visited: set[str] = set() # => mutable set, built up by explicit traversal
queue: deque[str] = deque(edges.get(start, [])) # => explicit FIFO work queue
while queue: # => explicit loop draining the queue
node = queue.popleft() # => explicit dequeue
if node in visited: # => explicit cycle guard
continue # => skip re-processing an already-visited node -- prevents the cycle from looping forever
visited.add(node) # => explicit mutation
queue.extend(edges.get(node, [])) # => explicit enqueue of newly discovered neighbors
return visited # => the fully built set
inference_result = reachable_via_inference("a", edges) # => run the logic-flavored version
bfs_result = reachable_via_bfs("a", edges) # => run the imperative version
print(sorted(inference_result)) # => a -> b,c; b -> d; d -> a: the cycle makes "a" reachable from itself too
# => Output: ['a', 'b', 'c', 'd']
print(inference_result == bfs_result) # => both must compute the identical reachable set
# => Output: TrueRun
python3 example.pyOutput
['a', 'b', 'c', 'd']
Truetest_example.py
"""Example 51: pytest verification for Logic vs Imperative Reachability."""
from example import reachable_via_bfs, reachable_via_inference
def test_both_approaches_agree_on_a_cyclic_graph() -> None:
edges = {"a": ["b", "c"], "b": ["d"], "c": [], "d": ["a"], "e": []} # => same graph as the demo
# => the a->b->d->a cycle makes "a" reachable from itself -- both approaches must agree it's included
assert reachable_via_inference("a", edges) == reachable_via_bfs("a", edges) == {"a", "b", "c", "d"}
def test_isolated_node_reaches_nothing_in_both_approaches() -> None:
edges = {"a": ["b", "c"], "b": ["d"], "c": [], "d": ["a"], "e": []} # => same graph
assert reachable_via_inference("e", edges) == reachable_via_bfs("e", edges) == set()
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: because a -> b -> d -> a is a cycle, "a" itself ends up in its own reachable set --
both the fixed-point inference and the explicit BFS agree on {'a', 'b', 'c', 'd'}, including the origin.
Why it matters: reachable_via_inference() keeps deriving new facts until a fixed point (no new
facts can be inferred) rather than draining an explicit queue -- the same underlying search, described
two different ways. Both approaches must handle the a -> b -> d -> a cycle without looping forever -- the BFS version guards with an explicit if node in visited: continue, while the inference version's while frontier - known: condition naturally stops once no new fact can be derived, a subtler but equally solid termination guarantee.
Example 52: Match-Case ADT Dispatch
ex-52 · exercises co-08, co-09
Three frozen dataclasses form a sum type (Shape = Circle | Rectangle | Triangle), and match/case
destructures the active variant's fields directly, with no isinstance chain.
example.py
"""Example 52: Match-Case ADT Dispatch."""
from dataclasses import dataclass # => @dataclass auto-generates __init__ for each variant below
@dataclass(frozen=True) # => variant #1 of the sum type
class Circle: # => frozen=True makes every instance immutable, safe to pattern-match against
radius: float # => the one field match/case can destructure below
@dataclass(frozen=True) # => variant #2 of the sum type
class Rectangle: # => frozen=True makes every instance immutable, safe to pattern-match against
width: float # => one of the two fields match/case can destructure below
height: float # => the other field match/case can destructure below
@dataclass(frozen=True) # => variant #3 of the sum type
class Triangle: # => frozen=True makes every instance immutable, safe to pattern-match against
base: float # => one of the two fields match/case can destructure below
height: float # => the other field match/case can destructure below
Shape = Circle | Rectangle | Triangle # => the SUM TYPE: a value is exactly one of these three variants
def area(shape: Shape) -> float: # => match/case destructures the ACTIVE variant, no isinstance chain
match shape: # => structural pattern matching over a union of dataclasses (PEP 634)
case Circle(radius=r): # => matches ONLY if shape is a Circle, binds its field to `r`
return 3.14159 * r * r # => classic circle-area formula, using the bound radius
case Rectangle(width=w, height=h): # => matches ONLY if shape is a Rectangle
return w * h # => classic rectangle-area formula, using the bound width and height
case Triangle(base=b, height=h): # => matches ONLY if shape is a Triangle
return 0.5 * b * h # => classic triangle-area formula, using the bound base and height
# => no wildcard case: Shape's three variants are exhaustive, every possibility is handled
shapes: list[Shape] = [Circle(2.0), Rectangle(3.0, 4.0), Triangle(5.0, 6.0)] # => one of each variant
areas = [round(area(s), 2) for s in shapes] # => dispatch each shape to its own matching case
print(areas) # => pi*4, 12, 15
# => Output: [12.57, 12.0, 15.0]Run
python3 example.pyOutput
[12.57, 12.0, 15.0]test_example.py
"""Example 52: pytest verification for Match-Case ADT Dispatch."""
from example import Circle, Rectangle, Triangle, area
def test_every_variant_is_handled_by_its_own_case() -> None:
assert round(area(Circle(2.0)), 2) == 12.57 # => circle variant
assert area(Rectangle(3.0, 4.0)) == 12.0 # => rectangle variant
assert area(Triangle(5.0, 6.0)) == 15.0 # => triangle variant
def test_dispatch_is_purely_structural_not_by_an_explicit_tag_field() -> None:
shapes: list[Circle | Rectangle | Triangle] = [Rectangle(1.0, 1.0), Circle(1.0)] # => mixed order
areas = [round(area(s), 2) for s in shapes] # => each dispatched to the correct case regardless of order
assert areas == [1.0, 3.14]
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: case Circle(radius=r): matches AND destructures in one step -- no isinstance(shape, Circle) check followed by a separate shape.radius access anywhere in area().
Why it matters: Example 27 dispatched on an explicit "kind" tag with meaning known only inside one
function; here the dispatch is purely structural -- the shape of the value itself decides which case
runs, and adding a fourth variant would need a fourth case, caught by exhaustiveness tooling if one is
missed.
Example 53: Enum State Tags
ex-53 · exercises co-04
A traffic light modeled as a plain enum.Enum with a declared TRANSITIONS table, instead of raw
strings compared by value.
example.py
"""Example 53: Enum State Tags."""
from enum import Enum # => plain enum.Enum, not StrEnum -- keeps this example runnable on Python 3.10+
class LightState(Enum): # => models states as a closed, named set of tags -- not raw strings
RED = "red" # => each member pairs a name with a value
YELLOW = "yellow" # => same shape as RED, a distinct named tag
GREEN = "green" # => same shape as RED, a distinct named tag
TRANSITIONS: dict[LightState, LightState] = { # => the full transition table, declared as data
LightState.RED: LightState.GREEN, # => red -> green
LightState.GREEN: LightState.YELLOW, # => green -> yellow
LightState.YELLOW: LightState.RED, # => yellow -> red, completing the cycle
} # => closes the transition table -- every LightState member has exactly one outgoing edge
def next_state(current: LightState) -> LightState: # => dispatch a transition by looking up the tag
return TRANSITIONS[current] # => a KeyError here would mean an unmodeled state -- fails loudly, not silently
state = LightState.RED # => start at RED
history = [state] # => record every state visited
# => the tag never leaks into raw string comparisons anywhere in this file
for _ in range(4): # => cycle through a full loop and then some, to prove it repeats correctly
state = next_state(state) # => rebind to the next tag via the transition table lookup
history.append(state) # => record this step before moving to the next iteration
print([s.value for s in history]) # => red -> green -> yellow -> red -> green
# => Output: ['red', 'green', 'yellow', 'red', 'green']Run
python3 example.pyOutput
['red', 'green', 'yellow', 'red', 'green']test_example.py
"""Example 53: pytest verification for Enum State Tags."""
from example import LightState, next_state
def test_full_transition_cycle_returns_to_the_start() -> None:
state = LightState.RED # => start fresh, isolated from the module-level demo
for _ in range(3): # => a full red -> green -> yellow -> red cycle
state = next_state(state)
assert state == LightState.RED # => back where we started after exactly three transitions
def test_every_state_has_a_defined_next_state() -> None:
for member in LightState: # => iterate every enum member -- proves the table is total, not partial
assert next_state(member) in LightState # => must resolve to some valid member, never KeyError
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: TRANSITIONS is total over LightState -- every member has a defined next state, so
next_state() can never silently accept a typo'd string the way a raw-string comparison could.
Why it matters: a closed enum plus a declared transition table is a lightweight version of the State
pattern from Example 34, trading dispatch-via-object-polymorphism for dispatch-via-dictionary-lookup. A raw string like "red" could be misspelled as "Red" or "redd" with no error until runtime; LightState.RED fails immediately at the point of the typo, since Python resolves enum member access at attribute-lookup time, not string-comparison time, catching the mistake far earlier than a string-tagged version would.
Example 54: Declarative Validation Rules
ex-54 · exercises co-08
A validation policy is stated as a list of Rule data records, and validate() evaluates them in
order, reporting exactly which declared rule failed.
example.py
"""Example 54: Declarative Validation Rules."""
from collections.abc import Callable, Mapping # => types the check function every Rule below carries
from dataclasses import dataclass # => @dataclass generates Rule's __init__ from its two fields
@dataclass(frozen=True) # => each rule is a plain DATA record: a name plus a check function
class Rule: # => frozen=True makes every Rule immutable once constructed
name: str # => the label reported when this rule fails
check: Callable[[Mapping[str, object]], bool] # => returns True if the input satisfies this rule
RULES: list[Rule] = [ # => the whole validation policy STATED as a list of data, not a chain of ifs
Rule("has_email", lambda data: "email" in data), # => rule #1: the key must be present at all
Rule("email_has_at_sign", lambda data: "@" in str(data.get("email", ""))), # => rule #2: crude shape check
Rule("age_is_non_negative", lambda data: int(data.get("age", 0)) >= 0), # => rule #3: a range constraint # type: ignore[call-overload]
] # => closes the declared policy -- adding a rule means appending one more line here
def validate(data: Mapping[str, object]) -> str | None: # => evaluate the rule list declaratively
for rule in RULES: # => walk the declared rules in order
if not rule.check(data): # => the first rule that fails IS the answer
return rule.name # => report exactly which declared rule was violated
return None # => every declared rule passed
good_input = {"email": "a@example.com", "age": 30} # => passes every rule
bad_input = {"email": "not-an-email", "age": 30} # => fails the second rule specifically
print(validate(good_input)) # => no rule failed
# => Output: None
print(validate(bad_input)) # => names the exact failing rule
# => Output: email_has_at_signRun
python3 example.pyOutput
None
email_has_at_signtest_example.py
"""Example 54: pytest verification for Declarative Validation Rules."""
from example import validate
def test_input_satisfying_every_rule_passes() -> None:
assert validate({"email": "a@example.com", "age": 30}) is None # => no declared rule was violated
def test_bad_input_is_flagged_with_the_specific_failing_rule() -> None:
assert validate({"email": "not-an-email", "age": 30}) == "email_has_at_sign" # => names the exact rule
assert validate({"age": 30}) == "has_email" # => a different missing-field failure names a different rule
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: RULES is a plain list of Rule(name, check) records -- adding, removing, or
reordering a validation rule never touches validate()'s own logic at all.
Why it matters: this is co-08's declarative style scaled to a real validation policy -- the policy
itself is data that could be logged, tested per-rule, or even loaded from configuration, none of which a
hard-coded if/elif chain of checks offers as naturally. Each Rule in RULES can also be unit-tested in isolation by calling its own check lambda directly, without invoking validate() at all -- a granularity an equivalent if/elif chain does not offer, since its individual conditions are not separately addressable values.
Example 55: Event Bus Pub/Sub
ex-55 · exercises co-16, co-17
A typed EventBus notifies every subscriber to a topic, independently -- two subscribers to the same
"order.created" topic each receive their own copy of the published payload.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
P["publish('order.created', payload)"]:::blue --> A["subscriber A"]:::orange
P --> B["subscriber B"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 55: Event Bus Pub/Sub."""
from collections.abc import Callable # => types every subscriber handler stored below
from dataclasses import dataclass, field # => @dataclass generates __init__; field() gives a fresh dict
from typing import TypeVar # => used to declare the generic payload type below
T = TypeVar("T") # => generic payload type, so the bus is reusable for any event shape
@dataclass # => auto-generates EventBus's __init__ from the field below
class EventBus: # => a TYPED publish/subscribe bus: multiple subscribers per topic
_subscribers: dict[str, list[Callable[[object], None]]] = field(default_factory=dict[str, list[Callable[[object], None]]]) # => topic -> handlers, one fresh dict per instance
def subscribe(self, topic: str, handler: Callable[[object], None]) -> None: # => register a subscriber
self._subscribers.setdefault(topic, []).append(handler) # => topics may have MANY subscribers
def publish(self, topic: str, payload: object) -> None: # => notify every subscriber to this topic
for handler in self._subscribers.get(topic, []): # => every registered handler gets a turn
handler(payload) # => called with the exact payload passed to publish()
bus = EventBus() # => construct a fresh bus
seen_by_a: list[object] = [] # => subscriber A's own recorder
seen_by_b: list[object] = [] # => subscriber B's own, independent recorder
bus.subscribe("order.created", lambda payload: seen_by_a.append(payload)) # => subscriber A
bus.subscribe("order.created", lambda payload: seen_by_b.append(payload)) # => subscriber B, same topic
bus.publish("order.created", {"id": 1}) # => BOTH subscribers must be notified once each
print(seen_by_a) # => A saw the payload
# => Output: [{'id': 1}]
print(seen_by_b) # => B saw the SAME payload, independently
# => Output: [{'id': 1}]Run
python3 example.pyOutput
[{'id': 1}]
[{'id': 1}]test_example.py
"""Example 55: pytest verification for Event Bus Pub/Sub."""
from example import EventBus
def test_all_subscribers_notified_exactly_once_each() -> None:
bus = EventBus() # => fresh bus, isolated from the module-level demo
counts = {"a": 0, "b": 0, "c": 0} # => local recorder for three subscribers
bus.subscribe("topic", lambda payload: counts.__setitem__("a", counts["a"] + 1))
bus.subscribe("topic", lambda payload: counts.__setitem__("b", counts["b"] + 1))
bus.subscribe("topic", lambda payload: counts.__setitem__("c", counts["c"] + 1))
bus.publish("topic", {"x": 1}) # => publish exactly once
assert counts == {"a": 1, "b": 1, "c": 1} # => every subscriber fired exactly once
def test_subscribers_on_a_different_topic_are_not_notified() -> None:
bus = EventBus() # => fresh bus
seen: list[object] = [] # => local recorder
bus.subscribe("topic.a", lambda payload: seen.append(payload)) # => subscribed to topic.a only
bus.publish("topic.b", {"unrelated": True}) # => publish to a DIFFERENT topic
assert seen == [] # => the topic.a subscriber was never called
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: seen_by_a and seen_by_b both end up holding [{'id': 1}] after one publish()
call -- pub/sub fans a single event out to every interested party, each of whom reacts independently.
Why it matters: Example 16's Dispatcher supported multiple handlers per event too, but naming it
as a "bus" with named topics is the pattern most real message queues and GUI event systems are built
on, one level up from a single dispatcher. seen_by_a and seen_by_b each independently hold the identical [{'id': 1}] after one publish() call, proving the bus fans out to every registered subscriber rather than picking just one -- the structural guarantee that distinguishes pub/sub from Example 16's single-topic dispatch.
Example 56: Reactive Debounce
ex-56 · exercises co-17
A DebouncedStream collapses a burst of rapid push() calls into just the last value, delivered only
when flush() fires.
example.py
"""Example 56: Reactive Debounce."""
from collections.abc import Callable # => types every downstream subscriber callback stored below
class DebouncedStream: # => a stream operator: collapses a burst of pushes into just the LAST value
def __init__(self) -> None: # => constructor starts with no pending value and no subscribers
self._pending: int | None = None # => the most recent value pushed during the current burst
self._downstream: list[Callable[[int], None]] = [] # => subscribers who only want the final value
def subscribe(self, fn: Callable[[int], None]) -> None: # => register a downstream listener
self._downstream.append(fn) # => append only -- does NOT call fn with anything yet
def push(self, value: int) -> None: # => called for every value in a burst -- does NOT notify yet
self._pending = value # => overwrite: only the latest value survives a burst
def flush(self) -> None: # => simulates "the debounce timer fired" -- deliver the last pending value
if self._pending is not None: # => only deliver if something was actually pushed since last flush
for fn in self._downstream: # => notify every subscriber with the FINAL value only
fn(self._pending) # => intermediate values 1 and 2 are never delivered to anyone
self._pending = None # => reset for the next burst
delivered: list[int] = [] # => records what downstream actually received
stream = DebouncedStream() # => construct
stream.subscribe(lambda v: delivered.append(v)) # => subscribe once
stream.push(1) # => burst: three rapid pushes
stream.push(2) # => intermediate values during a burst are NEVER delivered on their own
stream.push(3) # => only the last one, 3, matters
print(delivered) # => nothing delivered yet -- the burst hasn't been flushed
# => Output: []
stream.flush() # => the debounce timer "fires" -- only the LAST pushed value (3) reaches downstream
print(delivered) # => exactly one delivery, and it is the final value of the burst
# => Output: [3]Run
python3 example.pyOutput
[]
[3]test_example.py
"""Example 56: pytest verification for Reactive Debounce."""
from example import DebouncedStream
def test_only_the_final_value_of_a_burst_is_delivered() -> None:
stream = DebouncedStream() # => fresh stream, isolated from the module-level demo
delivered: list[int] = [] # => local recorder
stream.subscribe(lambda v: delivered.append(v))
for v in (10, 20, 30, 40): # => a burst of four rapid pushes
stream.push(v)
assert delivered == [] # => nothing delivered mid-burst
stream.flush() # => the burst ends
assert delivered == [40] # => only the final value survives
def test_a_second_burst_after_flush_delivers_its_own_final_value() -> None:
stream = DebouncedStream() # => fresh stream
delivered: list[int] = [] # => local recorder
stream.subscribe(lambda v: delivered.append(v))
stream.push(1)
stream.flush() # => first burst delivers 1
stream.push(99)
stream.push(100)
stream.flush() # => second burst delivers only its own final value
assert delivered == [1, 100] # => two independent flushes, each keeping only its burst's last value
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: delivered is empty after three push() calls and gains exactly one entry -- 3 --
after flush(); the intermediate 1 and 2 never reach a subscriber at all.
Why it matters: debouncing is a stream OPERATOR, not just a primitive value -- it demonstrates that
reactive systems compose (a raw Signal from Example 17 could sit behind this debounce, and a
Computed from Example 41 could sit downstream of it). delivered stays empty through all three push() calls and gains exactly the value 3, not 1 or 2, once flush() runs -- a measurable guarantee that a naive push-through-immediately reactive design (like Example 17's ObservableValue) would violate by delivering all three intermediate values.
Example 57: Dataflow Memoized Nodes
ex-57 · exercises co-18
A Node caches its computed value and tracks a _dirty flag, so invalidating an unrelated subtree
never triggers a wasted recompute of an unchanged node.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
S["source"]:::blue --> D["derived = source * 2"]:::teal
U["unrelated"]:::orange -.->|"invalidate() -- no edge to derived"| D
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
example.py
"""Example 57: Dataflow Memoized Nodes."""
from collections.abc import Callable
class Node: # => a dataflow graph node that caches its output and tracks whether it's stale
def __init__(self, compute: Callable[[], int], *deps: "Node") -> None:
self._compute = compute # => this node's own recompute rule
self._deps = deps # => the nodes this node depends on
self._dirty = True # => starts dirty -- nothing has been computed yet
self._cache: int | None = None # => the memoized result, once computed
self.compute_count = 0 # => counts ACTUAL recomputations -- proves memoization is working
def invalidate(self) -> None: # => mark this node (and implicitly its dependents) as needing recompute
self._dirty = True # => the cache is no longer trustworthy
def value(self) -> int: # => read the node's value, recomputing ONLY if dirty
if self._dirty: # => only recompute when something actually changed
self._cache = self._compute() # => run the rule
self.compute_count += 1 # => record that a real recomputation happened
self._dirty = False # => the cache is fresh again
return self._cache # type: ignore[return-value] # => guaranteed set after the branch above
source = Node(lambda: 5) # => a source node with no dependencies
unrelated = Node(lambda: 100) # => a SECOND, unrelated source node -- its own subtree
derived = Node(lambda: source.value() * 2, source) # => depends only on `source`, NOT on `unrelated`
print(derived.value(), derived.compute_count) # => first read: computes once
# => Output: 10 1
print(derived.value(), derived.compute_count) # => second read: cache hit, no recompute
# => Output: 10 1
unrelated.invalidate() # => invalidate the UNRELATED subtree only
print(derived.value(), derived.compute_count) # => derived's own cache is untouched -- still 1, not 2
# => Output: 10 1Run
python3 example.pyOutput
10 1
10 1
10 1test_example.py
"""Example 57: pytest verification for Dataflow Memoized Nodes."""
from example import Node
def test_repeated_reads_do_not_recompute_an_unchanged_node() -> None:
node = Node(lambda: 42) # => fresh node, isolated from the module-level demo
node.value() # => first read: real computation
node.value() # => second read: should be a cache hit
node.value() # => third read: should also be a cache hit
assert node.compute_count == 1 # => exactly one real recomputation across three reads
def test_invalidating_an_unrelated_subtree_never_recomputes_this_node() -> None:
source = Node(lambda: 5) # => fresh source
other = Node(lambda: 1) # => a completely separate, unrelated node
derived = Node(lambda: source.value() + 1, source) # => depends only on `source`
derived.value() # => compute once
other.invalidate() # => invalidate the unrelated node
derived.value() # => read again
assert derived.compute_count == 1 # => still just one recompute -- the unchanged subtree was skipped
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: derived.compute_count stays at 1 across three reads and one unrelated invalidation --
memoization skips recomputation entirely when nothing this node actually depends on has changed.
Why it matters: this is co-18's payoff stated as a measured guarantee rather than a claim --
expressing a computation as an explicit dependency graph lets a scheduler skip work with confidence,
because "unrelated" is a structural fact the graph itself can prove. derived.compute_count staying at
exactly 1 across three reads and one unrelated invalidation is a directly measured number, not an
assumption -- without the explicit _deps graph recording that derived only depends on source, a
scheduler would have no structural basis for skipping the recompute safely.
Example 58: Paradigm Cost Table
ex-58 · exercises co-23, co-24
Measures -- not guesses -- the cost difference between the imperative and declarative versions of
Example 9's task, via inspect.getsource() line counts and __code__.co_nlocals local-name counts.
example.py
"""Example 58: Paradigm Cost Table."""
import inspect # => inspect.getsource() reads a function's own source text, used by count_lines() below
from collections.abc import Callable # => types the plain-function argument shared by both metric helpers below
def evens_squared_imperative(nums: list[int]) -> list[int]: # => same task as example 9, measured here
result: list[int] = [] # => mutable accumulator -- one extra local name declarative avoids
for n in nums: # => explicit iteration over every input number
if n % 2 == 0: # => explicit selection: keep only even numbers
result.append(n * n) # => explicit mutate-in-place append of the squared value
return result # => the fully built accumulator
def evens_squared_declarative(nums: list[int]) -> list[int]: # => the declarative twin, measured here too
return [n * n for n in nums if n % 2 == 0] # => the same result, no named accumulator anywhere
def count_lines(fn: Callable[..., object]) -> int: # => a concrete, reproducible metric: physical lines of the function's body
return len(inspect.getsource(fn).strip().splitlines()) # => counts every line, def line included
def count_local_names(fn: Callable[..., object]) -> int: # => a second concrete metric: how many local variable names the body binds
code = fn.__code__ # => plain functions always carry __code__, pyright resolves it on Callable[..., object]
return code.co_nlocals - len(code.co_varnames[: code.co_argcount]) # => locals minus params
# => co_nlocals counts every local slot; subtracting the parameters leaves ONLY the names the body itself
# => introduces -- both versions need the loop variable `n`, but only imperative ALSO needs a separate
# => named accumulator (`result`) to collect results across iterations, one extra name declarative avoids
rows: list[tuple[str, int, int]] = [ # => the comparison table, built from ACTUAL measurements, not opinion
("imperative", count_lines(evens_squared_imperative), count_local_names(evens_squared_imperative)), # => row 1
("declarative", count_lines(evens_squared_declarative), count_local_names(evens_squared_declarative)), # => row 2
] # => closes the measured comparison table
for name, lines, local_names in rows: # => print the table, one row per paradigm
print(f"{name}: lines={lines} local_names={local_names}") # => the measured numbers, not an opinion
# => Output: imperative: lines=6 local_names=2
# => Output: declarative: lines=2 local_names=1
same_result = evens_squared_imperative([1, 2, 3, 4]) == evens_squared_declarative([1, 2, 3, 4]) # => equivalence check
print(same_result) # => the cost comparison is only meaningful because both versions are provably equivalent
# => Output: TrueRun
python3 example.pyOutput
imperative: lines=6 local_names=2
declarative: lines=2 local_names=1
Truetest_example.py
"""Example 58: pytest verification for Paradigm Cost Table."""
from example import count_lines, count_local_names, evens_squared_declarative, evens_squared_imperative
def test_declarative_version_measures_fewer_lines_and_local_names() -> None:
imperative_lines = count_lines(evens_squared_imperative) # => real measurement, not a guess
declarative_lines = count_lines(evens_squared_declarative) # => real measurement of the other version
assert declarative_lines < imperative_lines # => the concrete metric this example's claim rests on
imperative_names = count_local_names(evens_squared_imperative)
declarative_names = count_local_names(evens_squared_declarative)
assert declarative_names < imperative_names # => declarative never introduces a named accumulator
def test_both_measured_versions_are_still_behaviorally_equivalent() -> None:
nums = [1, 2, 3, 4, 5, 6] # => a cost comparison is meaningless if the versions disagree
assert evens_squared_imperative(nums) == evens_squared_declarative(nums) == [4, 16, 36]
# => Run: pytest -- Output: 2 passedVerify
pytest -qOutput
2 passedKey takeaway: count_lines() and count_local_names() are real introspection, not opinion --
evens_squared_declarative measures fewer lines (2 vs 6) and one fewer local name than its imperative
twin, for the identical result.
Why it matters: co-24's cost/benefit framing only means something when the cost is measured, not
asserted -- this example is the topic's methodology demonstrated on the smallest possible case before
Example 66's fuller decision table applies the same discipline more broadly. The measured numbers here --
6 lines and 2 local names for the imperative version versus 2 lines and 1 local name for the declarative
one -- are read directly off inspect.getsource() and __code__.co_nlocals, not estimated, which is
exactly the discipline co-24 asks for before declaring one paradigm "better" than another.
Last updated July 16, 2026