def countdown(n):
while n > 0:
yield n
n -= 1Generators
Before this lesson
P01.10.2 Iterables and iterators P01.4.3 The call stack
After this lesson you can
- Explain what calling a generator function does, and what each call to next does to its paused frame.
- Rewrite an iterator class as a generator function, and check that both give the same items.
- Write a generator expression, and explain why sum(x * x for x in data) holds one item at a time.
- Write batches(rows, size) and trace it on the study table: groups of 2, 2 and 1.
In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.
The question
The Countdown class of the last lesson took a class, three methods and an attribute to remember where it had got to, all to hand over 3, 2 and 1. A loop that prints them takes two lines. Could a function simply hand over a value, wait, and carry on from the same place when asked for the next?
From the last lesson
1. Which two built-in functions does a for loop use, and which exception ends it?
Show answer
iter, once, to get an iterator, and next, once per item. StopIteration ends it.
2. After c = Countdown(3), what are list(c) and then list(c) again?
Show answer
[3, 2, 1] and then []: the first call used the iterator up.
Intuition
An ordinary function runs from its first line to a return, and its frame is thrown away (P01.4.3). A generator function is a function that contains the word yield. Calling it does not run its body. It makes a generator: an object that holds the function’s frame, ready to run.
Each call to next puts that frame back on the call stack and runs it until it reaches a yield. The value after yield is handed to whoever called next, and the frame is taken off the stack again, paused, with every name in it kept. The next call carries on from the line after the yield.
A generator is a paused frame. next puts it back on the stack and runs it to the next yield; between calls it waits inside the generator object with its names intact.
So a generator is an iterator that writes itself: next gives the next item, and when the function’s body ends, the generator raises StopIteration.
Build it up
Step 1. Write it. Here is Countdown again, as a generator function.
Step 2. Call it. The call makes a generator object and runs none of the body.
gen = countdown(3)
print(type(gen).__name__)generator
Step 3. Ask for items. Each next runs the body to the next yield.
print(next(gen))
print(next(gen))
print(next(gen))
try:
next(gen)
except StopIteration:
print('StopIteration: the body has ended')3
2
1
StopIteration: the body has ended
The first next starts the body: n is 3, the loop test is True, and yield n hands over 3. The second carries on at n -= 1, loops, and hands over 2; the third hands over 1. The fourth lowers n to 0, the loop test fails, and the body ends, so next raises StopIteration.
Step 4. Watch the two sides take turns. Prints inside the generator show when its body runs.
def countdown_loud(n):
print(' start, n =', n)
while n > 0:
print(' yield', n)
yield n
n -= 1
print(' end')
gen = countdown_loud(2)
print('made the generator')
for x in gen:
print('got', x)made the generator
start, n = 2
yield 2
got 2
yield 1
got 1
end
The loop and the generator take turns. Nothing of the body runs until the first next, and each yield hands control back.
made the generator comes first, before start: the call ran nothing. After that the two sides take turns, one yield at a time.
The formal statement
The Python Tutorial (section 9.9) calls generators “a simple and powerful tool for creating iterators”, written “like regular functions but use the yield statement”. Its key line: “the local variables and execution state are automatically saved between calls”. The Functional Programming HOWTO puts the difference from return plainly: at a return, “the local variables are destroyed”; at a yield, “local variables are preserved”.
Lazy against eager. A list comprehension is eager: it works out every item at once and holds them all in a list. A generator expression is lazy: it works out an item only when next asks for one, and holds only the item in hand.
The same sum, two ways. The list comprehension holds five squares at once; the generator expression hands sum one square at a time.
Both give 18850. With five scores the difference does not matter. With a file of a hundred million rows, the list could need more memory than the computer has, while the generator still holds one row. PEP 289, which added generator expressions to Python, makes exactly this case, sum(x*x for x in range(10)), and says “Memory is conserved by using a generator expression instead”.
Worked example
Machine learning rarely feeds a model the whole table at once. It cuts the rows into small groups, mini-batches, and learns from one group at a time (ML01.5.4). Here is a generator that does the cutting.
def batches(rows, size):
batch = []
for row in rows:
batch.append(row)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batchTrace batches(rows, 2) on the five rows of the study table.
The trace of batches(rows, 2). A batch is handed over each time it reaches two rows, and once more at the end for the row that is left.
- Rows 1 and 2. Ama’s row goes in; the length is 1, not 2. Ben’s row goes in; the length is 2, so the batch of Ama and Ben is yielded, and the frame pauses.
- Rows 3 and 4. On the next
next, a fresh empty batch is made, and Chen and Dina fill it. It is yielded. - Row 5. Eli’s row goes in and the loop ends with one row in the batch. The test
if batch:is True for a list that is not empty, so the last, shorter batch is yielded too.
The sizes are 2, 2 and 1. By chance the mean score of every batch is 60.0: \({(55 + 65) / 2}\), \({(80 + 40) / 2}\) and \(60 / 1\).
Code
The worked example, and the mean score of each batch.
names = ['Ama', 'Ben', 'Chen', 'Dina', 'Eli']
hours = [2, 4, 5, 1, 3]
scores = [55, 65, 80, 40, 60]
rows = list(zip(names, hours, scores))
for batch in batches(rows, 2):
batch_scores = [score for name, h, score in batch]
print(len(batch), batch_scores, sum(batch_scores) / len(batch_scores))2 [55, 65] 60.0
2 [80, 40] 60.0
1 [60] 60.0
The standard library has the same tool since Python 3.12: itertools.batched hands over each batch as a tuple instead of a list.
from itertools import batched
print([len(b) for b in batched(rows, 2)])
print(list(countdown(3)), list(range(3, 0, -1)))[2, 2, 1]
[3, 2, 1] [3, 2, 1]
Now the two sums of squares. The generator expression needs no extra brackets when it is the only argument of a call.
print(sum([x * x for x in scores]))
print(sum(x * x for x in scores))
squares = (x * x for x in scores)
print(next(squares), next(squares))
print(sum(squares))18850
18850
3025 4225
11600
The last line prints 11600, not 18850: the two next calls had already taken 3025 and 4225, and sum added only the three squares that were left.
Explore
Lazy and eager pipeline
Ten numbers pass through a filter (is_even) and a map (square) to a consumer that keeps the first three. Press Step in eager mode, then in lazy mode, and compare the calls made and the items held. Then change how many the consumer keeps: when does lazy stop saving work?
The end of both runs. The lazy pipeline stops after six items: it makes 9 calls instead of 15 and holds 3 items instead of 23.
The two pipelines in code. The eager one is three list comprehensions; the lazy one is two generator expressions and a loop that asks for three items.
calls = []
def is_even(x):
calls.append('is_even')
return x % 2 == 0
def square(x):
calls.append('square')
return x * x
numbers = list(range(1, 11))
evens = [x for x in numbers if is_even(x)]
squared = [square(x) for x in evens]
first = squared[:3]
print('eager:', first, len(calls), 'calls')
calls = []
evens = (x for x in range(1, 11) if is_even(x))
squared = (square(x) for x in evens)
first = []
for k in range(3):
first.append(next(squared))
print('lazy: ', first, len(calls), 'calls')eager: [4, 16, 36] 15 calls
lazy: [4, 16, 36] 9 calls
The interactive counts the items held in lists. At the end the eager pipeline holds four lists: 10 numbers, 5 evens, 5 squares and the 3 kept, 23 items in all. The lazy one holds only the list first, 3 items; each generator holds one item while it works on it, and nothing once the loop has finished.
Pitfalls
Wrong beside right. A generator can be read once. Two answers from one generator need one pass that works out both.
“A generator is a list that is made later.”
It is an iterator, made of a paused frame. It has no len, no positions, and it is used up after one pass: sum(squares) followed by list(squares) gives an empty list the second time. If you need the items twice, or need len, make a list; if you need two answers, get both in one pass.
“Calling the generator function runs it.”
countdown_loud(2) on its own prints nothing. It makes a generator and waits. A generator function that is called but never looped over does no work at all, which is a surprise when the work was meant to be writing a file.
“return and yield do the same job.”
A return ends the call and throws its frame away. Write return batch in place of each yield batch in batches and you get the first batch only, as a list, and no loop over the rest. yield hands over a value and keeps the frame, ready for more.
Check yourself
Recall
1. What does calling a generator function return, and how much of its body runs?
Show answer
A generator object; none of the body runs until the first next.
2. What happens to the generator’s frame at a yield?
Show answer
The value is handed to the caller of next, and the frame is taken off the call stack and paused, with every name in it kept, ready to carry on from the next line.
3. When does a generator raise StopIteration?
Show answer
When its body ends, by reaching its last line or a return.
4. What is the difference between [x * x for x in scores] and (x * x for x in scores)?
Show answer
The first is a list comprehension: it builds a list of all five squares at once. The second is a generator expression: it makes a generator that works out one square each time it is asked.
5. What sizes do the batches of batches(rows, 2) have on the study table, and why is the last one shorter?
Show answer
2, 2 and 1. Five rows do not split into twos: one row is left when the loop ends, and if batch: yields it on its own.
Exercises
1. ★ Write a generator function evens_up_to(n) that yields 2, 4, 6 and so on up to n. What is list(evens_up_to(7))?
Show solution
def evens_up_to(n):
k = 2
while k <= n:
yield k
k += 2list(evens_up_to(7)) is [2, 4, 6]: after 6, k becomes 8, which is more than 7, so the body ends.
2. ★★ Predict what this prints, line by line.
gen = countdown(3)
print(next(gen))
print(list(gen))
print(list(gen))Show solution
3, then [2, 1], then []. The first next takes 3; list takes what is left; the generator is then used up, so the second list is empty.
3. ★★★ Use batches(rows, 2) and a generator expression to find, for each batch, the largest number of hours, as a list. Then say what batches(rows, 3) gives for the same question.
Show solution
[max(h for name, h, score in batch) for batch in batches(rows, 2)]. The batches hold the hours 2 and 4, then 5 and 1, then 3, so the answer is [4, 5, 3]. With size 3 the batches hold 2, 4, 5 and then 1, 3, so the answer is [5, 3].
Summary card
A generator is a paused frame: next runs it to the next yield.
A generator is a paused frame.
- Generator function: contains
yield; calling it makes a generator and runs nothing. - next: resumes the body, runs it to the next
yield, and returns the value; at the end,StopIteration. - Generator expression:
(expression for name in iterable), a lazy comprehension. - Lazy: one item in hand at a time; good for large data, but readable only once.
Sources and next steps
Sources. Each was opened and checked against this lesson on 12 November 2026.
- Python Software Foundation, The Python Tutorial, sections 9.9 and 9.10: generators are “a simple and powerful tool for creating iterators”; local variables and execution state are “automatically saved between calls”; generator expressions use “parentheses instead of square brackets” and “tend to be more memory friendly than equivalent list comprehensions”.
- Python Software Foundation, Glossary, entries “generator iterator” (“Each yield temporarily suspends processing, remembering the execution state”) and “generator expression”.
- Python Software Foundation, Functional Programming HOWTO, section “Generators”: at a
returnthe local variables are destroyed, while at ayieldthey are preserved. - Raymond Hettinger, PEP 289, Generator Expressions: the memory argument with
sum(x*x for x in range(10)), accepted for Python 2.4. - Python Software Foundation, itertools.batched: “Batch data from the iterable into tuples of length n. The last batch may be shorter than n.”; added in version 3.12.
Next. P01.10.4 Lazy pipelines and functions as arguments hands functions to other functions, and reads study.csv one row at a time.
This lesson unlocks
P01.10.4 Lazy pipelines and functions as arguments