Visual walkthrough — Generators — yield, generator functions, send(), next()
We derive the central result of the parent topic: a generator is a resumable stack frame — yield saves the current line and locals; next() restores them. No prior notation is assumed. If you have never seen yield, start at line one.
Step 1 — What a normal function's life looks like
WHAT. Look at the picture. A normal function add(a, b) opens a frame, runs top-to-bottom, produces one value with return, and then the frame is thrown away.
WHY this matters. Because the frame is destroyed, the function has no memory of its past. Call it again and it starts fresh from line one. This is exactly the limitation a generator will break.
PICTURE.

The amber marker is the program counter. Watch it slide from top to bottom, hit return, and then the whole frame (cyan box) evaporates — the dashed grey box on the right is nothing, deliberately empty.
Step 2 — The one word that changes everything: yield
WHAT. We replace return with yield. Now, when the program counter reaches yield i, it does not slide off the bottom. It stops, hands out i, and the frame survives.
WHY this tool and not return? return answers the question "what is the final answer?" — one value, then gone. yield answers a different question: "what is the next value in a sequence I will keep producing?" We need a verb that means "pause, don't die." That verb is yield.
PICTURE.

Same frame as Step 1, but now the amber marker parks on the yield line (glowing). The cyan frame is still solid — it did not evaporate. Note i = 0 is still sitting in its box, remembered.
Step 3 — Calling it builds a machine, it does NOT run
WHAT. g = count_up(3) constructs the frame but leaves the marker off the top edge, unstarted.
WHY. This is what makes generators cheap. Building the machine is free; it costs memory only for its own tiny frame, never for a giant list of results. count_up(10**12) is instant because nothing counted yet. (This is Lazy Evaluation in action.)
PICTURE.

The amber marker floats above the first line — armed, frozen, waiting. The result panel says only "g — a generator object", no numbers computed.
Step 4 — next(g): press "continue"
WHAT. First next(g): marker enters at the top, runs to yield i with i = 0, emits 0, freezes. Second next(g): marker resumes on the line right after yield, does i += 1 → i = 1, loops, hits yield i again, emits 1.
WHY. This is the payoff of Step 2's survived frame. Because the locals (i) and the counter were saved, resuming continues the count instead of restarting it. That is "remembering where you stopped."
PICTURE.

Two side-by-side frames: on the left the marker is entering the top (first next, emits 0); on the right the marker resumes just below the yield line (second next, i bumps to 1, emits 1). The cyan arrow shows the "resume-below-yield" jump — the heart of the whole idea.
Step 5 — The end: StopIteration
WHAT. After emitting 2, the next next(g) resumes, the loop condition i < n is now false (i = 3, n = 3), the counter slides off the bottom, and StopIteration is raised.
WHY. We need a distinct signal for "done" that is not itself a data value (a plain None would be ambiguous — maybe you meant to yield None). An exception is a clean out-of-band "finished" flag.
PICTURE.

The marker exits the bottom of the frame; the frame finally evaporates (like Step 1) and a red StopIteration flag fires. This is the only way a for loop knows to stop.
Step 6 — Degenerate case: an empty generator
WHAT. What if the body's while is never true? count_up(0): with n = 0, the condition 0 < 0 is false immediately.
WHY show this. The reader must never meet a case we skipped. A generator that yields nothing is legal and common (e.g. filtering with no matches). It must behave sanely.
PICTURE.

The marker enters the top, the loop test fails on the first check, the marker walks straight out the bottom — no yield is ever parked on — and StopIteration fires on the very first next(g). list(count_up(0)) is [].
Recall Check the empty and boundary cases
list(count_up(0)) gives what? ::: [] — the loop never runs, first next raises StopIteration.
list(count_up(1)) gives what? ::: [0] — yields 0 once, then stops.
Step 7 — Two-way traffic: send() makes yield an expression
WHAT. Trace the averager: x = yield average. On priming (next(g)) it emits the initial average (None) and freezes inside the yield. Then g.send(10) wakes it, and the frozen yield expression evaluates to 10, so x = 10; the body computes the new average and loops back to yield.
WHY prime first? Before the first yield is reached, there is no parked yield expression to receive a value. Sending non-None into an unstarted generator has nowhere to land — Python raises TypeError. You can only answer a question after it's been asked.
PICTURE.

Two amber arrows cross at the yield line: one arrow out (the average leaving) and one arrow in (the sent number w arriving to fill x). The cyan sidebar shows total and count accumulating across the resumes — proof the frame's memory persists. This two-way pause is the seed of Coroutines and async.
The one-picture summary

One frame, one amber marker, four labelled arrows:
- build (
g = f()) — marker parks above the top, nothing runs. - next / resume — marker enters or wakes, runs to the next
yield, emits outward. - send — the inward arrow filling
x = yield .... - StopIteration — marker exits the bottom, frame evaporates.
That single diagram is the entire generator model: a frame that remembers its line and its locals between visits.
Recall Feynman: the whole walkthrough in plain words
A normal function is a worker who does the whole job in one go and then goes home, forgetting everything. A generator is a worker who does one small step, freezes with their hand exactly where they left off, and waits. When you say "next!" they thaw, finish just up to the next freeze-point, hand you what they made, and freeze again — and crucially they still remember every number they'd written down (their locals). Their frozen pose is the saved game: the line they're on plus their notes. When you use send, you're not just saying "next!" — you're pressing a note into their frozen hand for them to read the instant they wake. When the worker finally walks out the door with no more steps left, they wave a red "StopIteration" flag so your for loop knows to stop asking. And if you gave them an empty job (count_up(0)), they walk straight out the door on the first "next!" — no steps, no numbers, just the flag. That freeze-remember-thaw cycle is the entire trick; everything else — lazy streaming, pipelines, coroutines — is just this same worker chained to more workers.