Topic 9.2
Generators and yield
In one line
A generator function uses yield to hand back values one at a time and freezes in place between them. It writes an iterator in a few lines, keeps its own state, and can produce infinite streams without using memory.
Think of it like this
A vending machine that makes one cup of tea each time you press the button, compared with a machine that makes a hundred cups in advance and lets them go cold. A generator makes the next value only when it is asked, then waits. It never prepares more than you asked for.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generator function
- A function that contains
yield. Calling it returns a generator object instead of running the body. - Generator object
- The iterator you get by calling a generator function. It runs the body step by step as you call
next(). - yield
- Hand back a value and pause the function there, keeping all its local variables.
- Resume
- Continue a paused generator from the line after its last
yield. - yield from
- Yield every value from another iterable, one by one, in place.
- send()
- Resume a generator and also give it a value, which becomes the result of the paused
yieldexpression. - Infinite stream
- A generator that never stops by itself, so the consumer decides when to stop.
- Streaming
- Handling data piece by piece as it arrives, instead of waiting for all of it.
Step by step
01A function that pauses
A normal function runs to the end and returns once. A generator function can return many times, because each yield returns a value and pauses.
The print lines in the first example run in a surprising order. Watch for it: calling the function prints nothing; the body starts at the first next().
def count_up_to(limit):
n = 1
while n <= limit:
yield n # hand back n, then freeze here
n += 1 # runs when the next value is asked for
for value in count_up_to(3):
print(value) # 1, 2, 302The life of a generator
A generator moves through four states: created (nothing has run), suspended (paused at a yield), running, and closed (finished). next() moves it from created or suspended to running, until the next yield or the end.
When the body ends, Python raises StopIteration for the caller. The value of a return statement in a generator becomes the .value of that StopIteration, but for loops ignore it.
03Why the generator is lazy
Because the body only runs between next() calls, no work is done until a value is needed. If the consumer stops after three values, the generator never computes the fourth. This is lazy evaluation.
The first runnable example below prints a line before and after each yield, so you can see the interleaving of the generator body and the caller.
04Infinite streams
A while True loop with a yield inside is a perfectly good generator. It will never end by itself, so the consumer must stop it. itertools.islice(gen, 8) takes only the first eight values.
Never call list() on an infinite generator: it tries to collect values forever and your program runs out of memory.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
from itertools import islice
print(list(islice(fibonacci(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13]05yield from: chaining generators
yield from stream is a shorter form of for item in stream: yield item. Use it to put something before or after another stream. In the example below it adds a [DONE] marker after every chunk of a fake model reply.
It matters most for recursion (walking a tree and yielding every node) and for code that needs send and close to reach the inner generator.
def with_end_marker(stream):
yield from stream # same as: for chunk in stream: yield chunk
yield "[DONE]"06send and close, briefly
yield is an expression, so it can have a value: value = yield total. The caller sets that value with gen.send(x). The first call must be next(gen) to run to the first yield; sending a non-None value to a just-created generator is an error.
gen.close() ends the generator by raising GeneratorExit inside it at the paused yield. A try/finally around the yield runs its finally block then, which is how a generator releases a file or a connection when the consumer quits early.
07A generator is single-use, like every iterator
A generator object is an iterator, so Topic 9.1 applies in full: once finished, it stays finished. To start again, call the generator function again to get a new generator object.
A generator also holds its paused local variables in memory until it is finished or deleted. That is small for one generator, but a program that keeps a million suspended generators alive uses real memory.
Try it yourself
- 1
Change the order of the output
In the first example, change
count_up_to(2)tocount_up_to(1)and remove one of theprint("got", next(gen))lines. Predict which bracketed lines will now appear, then run it. The "finished" line only appears when you ask for a value past the lastyield. - 2
Take more from the stream
In the second example, change
islice(fibonacci(), 8)toislice(fibonacci(), 20). Python integers never overflow, so you can ask for hundreds of values; the generator still uses almost no memory. - 3
Stream early and stop
Add a
breakafter the second chunk in thefor chunk in ...loop. The[DONE]marker is never produced, because the generator is never asked for it.
Code & diagrams
The lines in brackets come from inside the generator. Notice that creating the generator prints nothing, and each next() runs only up to the next yield.
Expected output
created the generator, nothing has run yet
(start)
(about to yield 1)
got 1
(resumed after 1)
(about to yield 2)
got 2
(resumed after 2)
(finished)
StopIteration: the generator is donefake_model_stream stands in for a real model API that streams its answer. The consuming loop is the same shape as real client code.
Expected output
first 8 Fibonacci numbers: [0, 1, 1, 2, 3, 5, 8, 13]
received: 'Generators '
received: 'stream '
received: 'tokens '
received: '[DONE]'
5
15
closedCounters are a toy. Real programs use generators to read big files one line at a time, cleaning each line as it goes. io.StringIO acts like an open file here, so no real file is needed. Notice that nothing is read until next() asks for it.
Expected output
nothing read yet: generator
alpha
beta
the rest: ['gamma']Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Ask a finished generator for more
Call next once too often on a generator.
def one_item():
yield "only"
gen = one_item()
print(next(gen))
print(next(gen))Break #2
Send a value to a generator that has not started
Call send(5) right after creating the generator.
def running_total():
total = 0
while True:
value = yield total
total += value
acc = running_total()
acc.send(5)Myth vs fact
Myth
Calling a generator function runs its code.
Fact
It only creates the generator object. The code runs a piece at a time when next() is called.
Myth
Generators are only for big data.
Fact
They are also the shortest way to write any custom iterator, and the natural way to write pipelines and streams, even for ten items.
Myth
yield and return do the same thing.
Fact
return ends the function for good. yield hands back a value and pauses, so the function can continue later.
Myth
A generator can be looped over again to get the same values.
Fact
It is an iterator and is used up after one pass. Call the function again for a fresh generator.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A generator function compiles to a normal code object with the generator flag set. Calling it builds a frame (the saved locals and instruction pointer) wrapped in a generator object.
yieldsuspends that frame without freeing it, andnext()resumes it. Per item a generator is usually a little slower than a list comprehension (there is a resume for every value), but it needs memory for one item instead of all of them, and it can stop early. Choose it for memory and laziness, not for raw speed. - ▸
inspect.getgeneratorstate(gen)reportsGEN_CREATED,GEN_SUSPENDED,GEN_RUNNINGorGEN_CLOSED. A generator cannot be resumed from inside itself: callingnext()on a generator that is running raisesValueError: generator already executing.gen.throw(SomeError)raises the error at the pausedyield, which lets the generator catch it and carry on (it then yields the next value), andgen.close()raisesGeneratorExitthere. A generator that catchesGeneratorExitand yields again makesclose()fail withRuntimeError: generator ignored GeneratorExit. Python also callsclose()for you when a suspended generator is garbage collected, which is what runs itsfinallyblock. - ▸
Since Python 3.7 (PEP 479) a
StopIterationthat leaks out of a generator body is turned intoRuntimeError: generator raised StopIteration. Before that, a straynext(it)inside a generator could silently end it early and hide a bug. To end a generator early, use a plainreturn, neverraise StopIteration. The same trap hides inside a helper: anext(iterator)with no default, called inside a generator, will turn an empty iterator into thisRuntimeError, so give it a default or catchStopIteration. - ▸
The
return valuein a generator becomesStopIteration.value, andresult = yield from inner()receives it. This was the foundation of the first coroutines;async/await(Phase 12) builds on the same suspend-and-resume idea with its own syntax. - ▸
Always put cleanup in
try/finallyaround theyield, or use a context manager (Topic 9.8). Without it, a consumer that stops early leaves the file or connection open until the generator is garbage collected. In CPython that happens as soon as the last reference goes (reference counting), but other implementations, or a generator stored in a long-lived structure, can delay it. For a guarantee, close the generator explicitly or wrap it incontextlib.closing(gen). - ▸
yield from inneris more than a loop: it forwardssend(),throw()andclose()toinner, and the whole expression evaluates to the value ofinner'sreturn. A recursive generator withyield from(walking a tree) therefore stays short. Async code has its own version,async defwithyield(an async generator, read withasync for), which Topic 16.5 uses for streaming.
Remember this
- 1
Recall from Topic 9.1: an iterator hands out one value per
next()call and raisesStopIterationwhen it is finished. A generator function looks like a normal function, but it contains the wordyield. Calling it does not run the body. It returns a generator object (an iterator) and nothing else happens yet. The body starts running only when something callsnext()on that object, which aforloop does for you. - 2
When the body reaches
yield value, the function handsvalueback and freezes exactly where it is: local variables, the position in the loop, everything. The nextnext()call resumes on the line after theyield. When the body ends (or reachesreturn), the generator raisesStopIterationfor you. This is the "state between yields" that makes generators so convenient. - 3
Because values are made on demand, a generator can describe an infinite stream. A generator that yields the Fibonacci numbers forever uses almost no memory, as long as the consumer stops after taking what it needs (
itertools.islice, which takes the first few values and is explained in Topic 9.4; abreak; ornext()a few times). A list of infinite length is impossible; a generator of infinite length is easy. - 4
yield from otherhands over to another iterable: every value fromotheris yielded in turn, as if you had written aforloop with ayieldinside. It is shorter, and it also passessend()andclose()through to the inner generator. It is the standard way to chain or flatten generators. - 5
Generators can also receive values.
value = yield resultpauses, and the caller can resume withgen.send(x), which makes the pausedyieldexpression evaluate tox. The first call must benext(gen)(orsend(None)) to run up to the firstyield.gen.close()stops the generator early by raisingGeneratorExitat the pausedyield. You will rarely needsend; plainyieldcovers most uses. - 6
Generators are how streamed model output is read. An API client gives you a generator that yields chunks of text as the model writes them. Your code loops over it and prints each chunk the moment it arrives, so the user sees the answer grow instead of waiting for the end (Topic 16.5 shows the real client). The example below uses a fake model so it runs here.
Explain it without notes
What happens when you call a generator function, and when does its body actually run?
How does a generator keep its state between values, and how is that different from an iterator class?
What is yield from for, and what does it do that a loop with yield does not?
Why can a generator describe an infinite sequence, and how do you use one safely?
What do return, raise StopIteration, close() and throw() each do inside or to a generator?
Practice
Write a generator squares(n) that yields the squares of 1 up to n. Print list(squares(5)).
Write a generator countdown(n) that yields n, n-1, ... down to 1, then the text "liftoff". Print each value on its own line with a for loop.
Write a generator chunks_of_text(text, size) that yields the text in pieces of at most size characters, then print the pieces for "streaming" with size 4.
Trade-offs
- ↔
Lazy and small, but single-use and not indexable. A generator holds one value at a time, so you cannot ask for
len(), slice it, or loop twice. Convert to a list when you need those. - ↔
Errors surface late. Because the body runs only when consumed, a bug inside the generator appears at the first
next(), far from the line that created it. Test generators by consuming them fully in a test.
Done when you can
Done when you can say what happens at the call of a generator function and at each
next().Done when you can write a generator with a loop and a
yield.Done when you can stop an infinite generator safely with
isliceorbreak.Done when you can use
yield fromto chain two generators.Done when you can explain what
sendandclosedo, and when each is allowed.