1.3.9 · D5Python Intermediate
Question bank — Generators — yield, generator functions, send(), next()
This bank targets the traps around laziness, single-use exhaustion, the priming ritual for send(), and the way return/StopIteration/throw() behave differently than you expect. See also Iterators and the Iterator Protocol, Lazy Evaluation, and StopIteration and Exceptions.
The lifecycle picture (read this first)
Before the questions, hold this mental movie of a generator's five states: created (frozen), primed/running to a yield, paused at a yield, resumed, and exhausted. The figure below is the map every trap on this page hides in.

The helper functions used below (in-page reference)
True or false — justify
Calling a generator function immediately runs the code in its body.
False — calling it only builds and returns a generator object; the body stays frozen and first runs on the first
next(). A print at the top of the function will not fire until you pull a value.A generator object is its own iterator, so iter(g) is g is True.
True — a generator implements both
__iter__ (returning itself) and __next__, so it satisfies the Iterators and the Iterator Protocol as a single self-iterating object.You can loop over the same generator object twice and get the same values both times.
False — a generator is single-use; once it raises
StopIteration, it is exhausted forever, so a second list(g) yields []. Re-create it by calling the function again.next(g) and g.send(None) do exactly the same thing.
True — a plain
next(g) is defined as g.send(None); it advances to the next yield and injects None as the value of any paused yield expression.A function with yield inside an if that never runs is still a generator function.
True — Python decides "generator or not" statically by scanning the body for the
yield keyword, regardless of whether that line is ever reached at runtime.return 5 inside a generator makes next(g) return 5 to the caller.
False — in a generator
return 5 raises StopIteration whose .value attribute is 5; the caller's for/next treats it as "done", not as a yielded 5.An infinite while True: yield n loop will hang your program the moment you create the generator.
False — nothing runs at creation; the loop only advances one step per
next(), so it is safe as long as you pull a finite number of values (see Lazy Evaluation).yield from some_list is just a shorthand for for x in some_list: yield x.
Mostly true for plain iteration, but
yield from also forwards send() values, throw() exceptions, and the sub-generator's return value — the manual loop does not.A generator expression like (x*x for x in data) builds the squared list immediately.
False — a generator expression is lazy like a generator function; it computes each value only when pulled, unlike the eager list comprehension (see List Comprehensions vs Generator Expressions).
Two generators created from the same function share state.
False — each call to the function creates a fresh, independent generator object with its own local variables and its own paused position.
g.throw(ValueError) just raises the error in your calling code like a normal raise.
False —
throw injects the exception at the paused yield inside the generator, so the generator's own try/except around that yield can catch and handle it before your code ever sees it.Spot the error
g = averager(); g.send(10) — what breaks and why?
It raises
TypeError: can't send non-None value to a just-started generator. The generator hasn't reached its first yield yet, so there is no paused yield expression to receive 10; you must prime with next(g) first.for x in count_up(3): pass then later for x in count_up(3): pass — is the second loop empty?
No —
count_up(3) is called again, producing a brand-new generator, so the second loop runs fully. The exhaustion trap only bites when you reuse the same generator object.def f():
print("start")
yield 1
g = f()Nothing printed — bug or expected?
Expected. Building
g does not execute the body, so "start" prints only on the first next(g). This is the classic "calling ≠ running" trap.total = squares(read_numbers(data)); print(len(total)) — why the error?
A generator has no
__len__, so len() raises TypeError. Generators don't know their length in advance (they may be infinite); wrap in list(...) first if you truly need a count.result = list(gen); print(result); print(list(gen)) prints the data then []. Bug?
Not a bug — expected exhaustion. The first
list(gen) consumes every value; the generator is now spent, so the second list(gen) is empty.Inside a generator, x = yield with no value on the right — is that valid?
Yes — it yields
None out and waits to receive a value in on resume. It is the standard "receive-only" coroutine pattern (see Coroutines and async).g.throw(ValueError) on a generator with no try around its yield — what happens?
The exception propagates out of the generator to your calling line (as if raised there) and leaves the generator exhausted, because nothing inside caught it at the paused
yield.Why questions
Why does for never crash with StopIteration even though the generator raises it?
The
for statement is built to catch StopIteration internally and stop the loop silently; the exception is the loop's private "we're done" signal, not an error you see (see StopIteration and Exceptions).Why must you prime a send()-based coroutine with next(g)?
Because you can only answer a question after it has been asked. Priming runs the body up to the first
yield, creating a paused expression that a later send(value) can then fill in.Why is count_up(10**12) cheap in memory when the equivalent list would crash?
The generator never materializes all values — it holds only the current local state and a promise to produce the next value on demand, which is the whole point of Lazy Evaluation and Memory and Big Data Streaming.
Why does x = yield value do two things at once?
The
yield sends value out to the caller when it pauses, and when resumed via send(v) the entire yield expression evaluates to v, feeding data back in. One keyword, two directions.Why does chaining generators (a pipeline) not load all the data at once?
Each stage pulls one item from the previous stage only when its own
next() is called, so a single element flows end-to-end at a time — never the whole dataset (see Memory and Big Data Streaming).Why can a hand-written class with __next__ mimic a generator, proving there's "no magic"?
Because a generator is just an object with
__iter__ and __next__; the only thing the interpreter adds is automatic saving of local variables and the current line as the paused state.Why does re-yielding with yield from matter beyond convenience?
It transparently forwards
send(), throw() exceptions, and the sub-generator's return value to/from the delegated generator — a manual for x in it: yield x loop swallows all three, so yield from is essential for Coroutines and async pipelines.Why does throw() exist alongside send() and close()?
send() pushes a value in and close() requests shutdown; throw() pushes an exception in at the paused yield, letting the generator react to errors (e.g. clean up or switch behaviour) exactly where it left off.Edge cases
What does next(g) do after the generator has already finished once?
It raises
StopIteration again immediately; an exhausted generator stays exhausted and never restarts on its own.Why does a bare yield (no value after it) hand out None?
yield is an expression whose "out" slot is whatever you write after it; with nothing there the out-value defaults to None — exactly like a return with no value returns None. So next(g) on while True: yield always returns None and never raises StopIteration.If a generator's return x runs, how do you actually read x?
Catch the
StopIteration it raises and read the exception's .value attribute, or use yield from in a delegating generator which captures that return value automatically.What happens if you call close() on a generator paused at a yield?
Python throws
GeneratorExit at the paused yield; if the generator doesn't catch it (or re-yields), it terminates cleanly and future next() calls raise StopIteration.What if a generator catches GeneratorExit and then yields again?
Python raises
RuntimeError — a generator is not allowed to yield during close; it must let GeneratorExit propagate or return without yielding.What does an empty generator (a function that yields nothing before returning) produce?
It is a valid, immediately-exhausted iterator — the first
next() raises StopIteration, and list(g) gives []. It still counts as a generator because yield appears in its body.Can a generator expression be nested inside another lazily?
Yes —
(y for y in (x*x for x in data)) stays fully lazy end-to-end, each layer pulling one value at a time, exactly like a chained generator-function pipeline.Recall One-line self-test
If you can explain why priming is required, why looping twice fails, why return becomes StopIteration.value, and what throw() does at the paused yield, you have caught the deepest traps this topic sets.