1.3.9 · D4Python Intermediate

Exercises — Generators — yield, generator functions, send(), next()

2,373 words11 min readBack to topic

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, returns 1. No yield.
  • b()generator object. Has yield.
  • c()normal, prints "hi" and returns [1, 2, 3]. No yield.
  • d()generator object. Has yield.

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, then x = 2
  • x = 2 → yield 2, then x = 4
  • x = 4 → yield 4, then x = 8
  • x = 8 (still <= 8) → yield 8, then x = 16
  • x = 16 fails x <= 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 out

evens() 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 to yield average, average is still None. Prints None.
  • (B) send(4)x = 4, total = 4.0, count = 1, average = 4.0. Prints 4.0.
  • (C) send(10)x = 10, total = 14.0, count = 2, average = 7.0. Prints 7.0.
  • (D) send(10)x = 10, total = 24.0, count = 3, average = 8.0. Prints 8.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:

  1. squares needs n, so it calls next() on read.
  2. read reads "3\n", strips, int3, yields it.
  3. squares computes 3 * 3 = 9, yields 9.
  4. 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-yields 1, then 2.
  • When counter hits return 99, it raises StopIteration(99). Because we used yield from, that 99 becomes the value of the yield from expression, so result = 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 oldest

Case 1windows([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.
Figure — Generators — yield, generator functions, send(), next()

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=4
  • x=4,y=2 → yield 2, y=3
  • x=4,y=3 → yield 3, y=8
  • x=4,y=8 → yield 4, x=7
  • x=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 [].