Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Iteration & Laziness  ›  Lesson

Generators: Functions That Pause

Generators 22 minA function with yield returns an iterator whose frame parks at every yield and resumes, locals intact, at the next next()
You're building a piece ofQuarry — the feed
This piece — read_lines(): Turns fixed-size chunks of a file back into whole lines, holding only one chunk at a time.
Scenario Quarry reads its 200 MB server log in fixed-size chunks, and the chunk boundaries fall in the middle of lines.
Your task
Write the generator lines_from_chunks(chunks), which takes any iterable of text chunks and yields every complete line without its "\n", carrying the unfinished tail from one chunk into the next and yielding the leftover at the end if it isn't empty. read_lines(text, size) (provided) returns the lines as a list. Example: read_lines("a\nbb\nccc", 2) → ["a", "bb", "ccc"].

Generators: Functions That Pause

In short: write an ordinary function, put yield where you'd return a value, and Python builds lesson 01's two-method, one-exception iterator for you. Calling a generator function runs none of its body — it returns a generator object immediately, and the body only starts at the first next(). Each next() runs to the next yield, hands back that value, and parks the frame: every local variable survives, waiting for the call after. A generator is still a stream, not a container — walk it twice and the second walk is empty. The piece you'll build is Quarry's line feed: files are read in fixed-size chunks, and a chunk boundary can land in the middle of a line.

A generator's frame running to a yield, then parked (greyed, paused) holding its locals, then resuming on the next next() call exactly where it left off, n incrementing only then

Rewrite Quarry's row cursor in five lines

Lesson 01's row cursor took two classes, a position attribute, and a hand-raised StopIteration. A generator function — a function whose body contains yield — does the same job in five lines:

python

No pos, no __next__, no raise. The for loop inside the function is the position, and when the function finishes, Python raises StopIteration for you. The mistake almost everyone makes on day one: assuming the function's body runs when you call it.

What does this print?
def feed():
    print("open", end=" ")
    yield 1
    print("close", end=" ")

g = feed()
print("made", end=" ")
print(next(g))

type(g).__name__ is "generator", and iter(g) is g is True — a generator is an ordinary iterator of the kind you wrote by hand in lesson 01, which is why it's a stream: walk it twice and the second walk is empty.

Watch a generator's frame pause and resume

When you call an ordinary function, Python makes a frame — the call's local variables and the line it's on — pushes it on the call stack, and discards it on return. A generator's frame is different: at each yield it comes off the stack but is kept, every local intact. The next next() puts that same frame back on, resuming right after the yield.

Watch the stack as a loop pulls three lines from a numbering generator:

watch it move · stack

A generator's frame leaves the call stack at every yield and comes back at the next next(). Its locals survive in between.

next(g) pushes the generator's frame, and it runs until the first yield. Stack, 2 items. Top is numbered() · n=1, line='a'.

1 / 6

The same thing, told as a conversation between the loop and the generator:

Drawing diagram…

Now step through the real thing. Notice how the trace jumps into the generator's body at each next() and back out at each yield, and that n still holds its old value every time it jumps back in:

step through it
1def numbered(lines):
2 n = 0
3 for line in lines:
4 n += 1
5 yield f"{n}:{line}"
6 
7g = numbered(["a", "b", "c"])
8first = next(g)
9second = next(g)
10rest = list(g)
11print(first, second, rest)
What does this print?
def numbered(lines):
    n = 0
    for line in lines:
        n += 1
        yield f"{n}:{line}"

g = numbered(["a", "b", "c"])
next(g)
print(next(g), end=" ")
print(list(g))

inspect.getgeneratorstate(g) confirms the three states directly: freshly made (GEN_CREATED), parked at a yield (GEN_SUSPENDED), and finished (GEN_CLOSED) — worth a look if you want to see the lifecycle by name.

Read a file in chunks without loading it

Quarry never reads a 200 MB file with .read() — that puts the whole file in memory. It reads a fixed-size chunk at a time, and a generator is the natural shape:

python

Look at where the boundaries fall: 'boot o', 'k\ndisk'. A chunk has no idea where a line begins or ends, so the feed has to carry each chunk's unfinished tail forward and glue it onto the next one — only complete lines leave. The carry never grows past one chunk plus one line, however big the file gets, which is the whole point of streaming: memory stays flat while the file grows.

Trap: inside a generator, return means "stop right now" — it raises StopIteration immediately, which is an easy way to break a generator with one wrong word:

broken — fix it

Run it and look closely at what list() received. Then change one word so every non-blank line comes through.

Whether a function is a generator is decided by whether yield appears anywhere in its body — settled when the function is compiled, not when it runs. One stray return where you meant yield turns a generator back into an ordinary function, silently.

Quick practice

What does this print?
import sys

small = (n for n in range(10))
huge = (n for n in range(10_000_000))
print(sys.getsizeof(small) == sys.getsizeof(huge))
What does this print?
def once():
    yield "a"
    yield "b"

g = once()
print(list(g), list(g))
What does this print?
def first_word(lines):
    for line in lines:
        if line:
            yield line.split()[0]

words = first_word(["boot ok", "", "disk warm"])
print(next(words), next(words))

🎯 Your turn

Write the generator lines_from_chunks(chunks). It takes any iterable of text chunks and yields every complete line without its "\n". It carries the unfinished tail from one chunk to the next, and at the end yields whatever is left over if it isn't empty.

chunks(text, size) is written for you. It stands in for reading a file size characters at a time. Then read_lines(text, size) returns list(lines_from_chunks(chunks(text, size))):

  • read_lines("boot ok\nwarm\n", 4) → ["boot ok", "warm"]
  • read_lines("a\nbb\nccc", 2) → ["a", "bb", "ccc"] (no final newline)
  • read_lines("one\n\nthree\n", 1) → ["one", "", "three"] (a blank line is still a line)

Hint: carry += chunk, then *done, carry = carry.split("\n"). Everything in done is a complete line.

Press ▶ Run, then ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upLesson 01's countdown, in four lines

Lesson 01 built Countdown as a class with __iter__ and __next__. Write it as a generator: count_down(start) yields start, start - 1, … down to 1. countdown(start) (already written) returns the list.

generator-pipelinesyield
DrillThe highest reading so far

Write the generator running_max(values), which yields, for each value in turn, the largest value seen so far. maxima(values) returns the list. The point is that best survives between yields without any class.

generator-pipelinesgenerator-state
BuildRecords with a two-character separator

Some exports separate records with "||" rather than a newline, and a chunk boundary can fall between the two bars. Write the generator split_records(chunks, sep), which yields every record in order, carrying the unfinished tail across chunks and yielding the leftover at the end if it isn't empty. records(chunks, sep) returns the list.

generator-pipelineschunked-reading
BossTake turns until everyone runs out

Quarry samples several sources fairly by taking one item from each in turn. Write the generator round_robin(*sources), which yields one item from each source, then goes round again, dropping each source once it runs out, until none are left. interleave(lists) returns the list.

generator-pipelinesgenerator-stateiteration-protocol
CapstoneThe last n lines of a huge file

Quarry's error view shows the last n lines of a log that may be far bigger than memory. chunks and lines_from_chunks are provided. Write tail(text, size, n), which streams the lines and returns the last n as a list, never holding more than n lines at once. n of 0 or less gives [].

generator-pipelineschunked-reading
ExplainWhere the variables wait

In an ordinary function, local variables disappear when the function returns. Yet a generator's n still holds 2 when you call next() for the third time. Explain where a generator's local variables live between next() calls, what happens to them at a yield, and when they are finally thrown away.

read_lines.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
read_lines(text, size) → listRead `text` in chunks of `size` characters and return the whole lines, rebuilt across chunk boundaries.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.