Exercises — Generators — yield, generator functions, send(), next()
Level 1 — Recognition
You are only asked to recognise what a generator is and what it does — no tricky state yet.
Exercise 1.1 — Is it a generator?
Recall Solution
Rule: a function is a generator function if and only if its body contains the keyword yield anywhere (even unreached). Calling it then returns a generator object without running the body.
a()→ normal, returns1. Noyield.b()→ generator object. Hasyield.c()→ normal, prints"hi"and returns[1, 2, 3]. Noyield.d()→ generator object. Hasyield.
Note: b() and d() do not print or loop when called — the body only runs when you pull values with next().
Exercise 1.2 — Count the outputs
Recall Solution
There are 3 yield statements, each reachable exactly once. So next() succeeds 3 times (returning 10, 20, 30), and the 4th call raises StopIteration.
Picture the tap analogy: three sips of water are in the pipe. The fourth turn of the tap gives nothing — the "empty" signal is StopIteration.
Level 2 — Application
Now you use generators with next(), for, and list().
Exercise 2.1 — Trace the values
Recall Solution
Walk the loop, writing down each value at the moment we hit yield:
x = 1→ yield 1, thenx = 2x = 2→ yield 2, thenx = 4x = 4→ yield 4, thenx = 8x = 8(still<= 8) → yield 8, thenx = 16x = 16failsx <= 8→ loop ends →StopIteration
list(...) collects every yielded value until StopIteration:
Exercise 2.2 — Take exactly k
Recall Solution
def take(gen, k):
out = []
for _ in range(k):
out.append(next(gen)) # pull one value, k times
return outevens() is an infinite generator, but that is safe because it is lazy — values only materialise when pulled. Pulling 4 gives:
(See Lazy Evaluation for why an infinite loop is harmless inside a generator.)
Level 3 — Analysis
Here you reason about state, priming, and pipelines.
Exercise 3.1 — The averager coroutine
Recall Solution
x = yield average sends average out, and the value you send in becomes x. Trace carefully:
- (A)
next(g)primes: runs toyield average,averageis stillNone. PrintsNone. - (B)
send(4)→x = 4,total = 4.0,count = 1,average = 4.0. Prints4.0. - (C)
send(10)→x = 10,total = 14.0,count = 2,average = 7.0. Prints7.0. - (D)
send(10)→x = 10,total = 24.0,count = 3,average = 8.0. Prints8.0.
Output: None, 4.0, 7.0, 8.0. This is a running average — see Coroutines and async for where send() leads.
Exercise 3.2 — Pipeline order of operations
Recall Solution
Building pipe runs nothing — both generators are lazy. When list(pipe) asks for the first value:
squaresneedsn, so it callsnext()onread.readreads"3\n", strips,int→3, yields it.squarescomputes3 * 3 = 9, yields 9.- Repeat for
"5\n"→5 * 5 = 25.
Only one element flows through the whole pipeline at a time — this is the streaming property (see Memory and Big Data Streaming).
Level 4 — Synthesis
Combine multiple features to build something.
Exercise 4.1 — yield from delegation
Recall Solution
yield from sub re-yields every value of sub, one at a time, as if you had written for x in sub: yield x. So:
- from
[1, 2]→1,2 - from
[3]→3 - from
[4, 5, 6]→4,5,6
Exercise 4.2 — Capture a generator's return value
Recall Solution
yield from counter()re-yields1, then2.- When
counterhitsreturn 99, it raisesStopIteration(99). Because we usedyield from, that99becomes the value of theyield fromexpression, soresult = 99. print("captured: 99")fires.- Then
yield 100.
The list() collects only the yielded values (1, 2, 100) — the 99 is captured, not yielded:
captured: 99
[1, 2, 100]
Level 5 — Mastery
Design a non-trivial generator from scratch and reason about its edge cases.
Exercise 5.1 — A moving_window generator
Recall Solution
Keep a buffer of the last size items; yield it whenever it is full, then drop the oldest:
def windows(iterable, size):
buf = []
for item in iterable:
buf.append(item)
if len(buf) == size:
yield tuple(buf) # emit current window
buf.pop(0) # slide: drop oldestCase 1 — windows([1, 2, 3, 4], 2):
- buf
[1](not full), buf[1,2]→ yield(1,2), drop →[2] - buf
[2,3]→ yield(2,3), drop →[3] - buf
[3,4]→ yield(3,4)Case 2 (degenerate) —windows([1, 2], 3): the buffer never reaches length 3, so nothing is yielded: This graceful "empty when input too short" is exactly the edge case you must always check.

Exercise 5.2 — Lazy merge of two sorted streams
Recall Solution
Peek one item from each side; yield the smaller; refill only that side. Handle exhaustion of either side (StopIteration) by flushing the rest — see StopIteration and Exceptions.
def merge(a, b):
ia, ib = iter(a), iter(b)
x = next(ia, None) # None = "side a done"
y = next(ib, None)
while x is not None and y is not None:
if x <= y:
yield x
x = next(ia, None)
else:
yield y
y = next(ib, None)
# flush whichever side remains
while x is not None:
yield x
x = next(ia, None)
while y is not None:
yield y
y = next(ib, None)Trace merge([1,4,7], [2,3,8]):
x=1,y=2→ 1 ≤ 2 → yield 1, x=4x=4,y=2→ yield 2, y=3x=4,y=3→ yield 3, y=8x=4,y=8→ yield 4, x=7x=7,y=8→ yield 7, x=None- a exhausted → flush b: yield 8 Every value is produced by pulling one item at a time — never loading both lists fully. This is the core idea behind generator expressions used on huge files.
Recall Master checklist (reveal after you finish)
Calling a generator function returns what? ::: A generator object — the body has not run yet.
How many next() calls before StopIteration for a body with 3 reachable yields? ::: 3.
Can you loop a generator twice? ::: No — it is single-use / exhausted after the first pass.
Why must you prime a coroutine before send(value)? ::: So it reaches its first yield and has a paused expression to receive the value; otherwise TypeError.
Where does return x go inside a generator? ::: Into StopIteration.value, capturable only by yield from.
What does windows([1,2], 3) yield? ::: Nothing — buffer never fills, so [].