Skip to main content

Generators

A function that contains yield returns a generator: its frame pauses at each yield and resumes at the next call to next. Countdown in four lines, generator expressions as lazy comprehensions, and batches that cut the study table into the mini-batches of machine learning.
P01.10.3L0 Foundation25 minIn review

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.

Two panels drawn as call stacks, the global frame at the top. Left, during next(gen): the global frame holds gen, an arrow to a generator object, and below it the frame of countdown is on the stack and running, with n bound to 3 and the line yield n filled yellow. Right, between calls: only the global frame is on the stack; it now also holds x bound to 3, the value handed over. The frame of countdown sits inside the generator object, off the stack, drawn dashed and marked paused after yield n, still holding n bound to 3.
P01.10.3-F1

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.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

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
Two lanes side by side, the for loop on the left and the frame of countdown_loud(2) on the right, with time running downwards. The loop calls next(gen) and an arrow crosses to the right: the body starts, prints start, n = 2, then yield 2, and an arrow carries 2 back to the loop, which prints got 2. The second next(gen) crosses over again: the body carries on at n -= 1, prints yield 1 and hands back 1; the loop prints got 1. The third next(gen) crosses over: n becomes 0, the loop ends, the body prints end and finishes, and StopIteration comes back, which ends the for loop. Before the first call a note says made the generator is printed first: calling countdown_loud(2) ran no line of its body.
P01.10.3-F2

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

NoteGenerators
  • A generator function is a function whose body contains yield. Calling it returns a generator and runs none of the body.
  • A generator is an iterator. Each next(gen) resumes the body where it stopped and runs it to the next yield value; next returns that value and the frame pauses, keeping its names.
  • When the body ends (it reaches its last line or a return), next raises StopIteration. A generator is then used up, like every iterator.
  • A generator expression is a comprehension in round brackets, (expression for name in iterable if condition). It makes a generator that works out one item each time it is asked.

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.

Two panels. Left, eager, sum([x * x for x in scores]): the list comprehension first builds a list of five slots, 3025, 4225, 6400, 1600 and 3600, all in memory at once, and then sum adds them to 18850. Right, lazy, sum(x * x for x in scores): sum asks a generator for one square at a time; the picture is caught on the third item, with the generator's paused frame holding x bound to 80 and the square 6400 on its way to sum, whose total so far is 7250 and becomes 13650. One item is in hand at a time, and the total at the end is the same, 18850.
P01.10.3-F4

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 batch

Trace batches(rows, 2) on the five rows of the study table.

A trace table for batches(rows, 2) with the columns step, row, batch after append and what happens. Step 1, Ama's row: the batch holds Ama, its length 1 is not 2. Step 2, Ben's row: the batch holds Ama and Ben, length 2, so it yields that batch, filled yellow, and starts a new empty one. Step 3, Chen: the batch holds Chen. Step 4, Dina: Chen and Dina, yielded. Step 5, Eli: Eli alone. After the loop the batch is not empty, so it yields Eli. Below: three batches of sizes 2, 2 and 1, whose mean scores are 60.0, 60.0 and 60.0.
P01.10.3-F3

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

P01.10-X1

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?

Two rows of ten numbered items passing through is_even and square to a consumer that keeps the first three. Eager: all ten items are made and tested, a list of five even numbers and a list of five squares are built, and three are kept: 15 calls and 23 items held in lists. Lazy: only items 1 to 6 are read, items 7 to 10 are never made, three squares are made and kept: 9 calls and 3 items held.
Two rows of ten numbered items passing through is_even and square to a consumer that keeps the first three. Eager: all ten items are made and tested, a list of five even numbers and a list of five squares are built, and three are kept: 15 calls and 23 items held in lists. Lazy: only items 1 to 6 are read, items 7 to 10 are never made, three squares are made and kept: 9 calls and 3 items held.
P01.10.3-F6

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, with a cross: squares = (x * x for x in scores), then total = sum(squares) gives 18850, then count = len(list(squares)) gives 0, because the generator was used up by sum, so total / count raises ZeroDivisionError. Right, with a tick: one pass that keeps both numbers, a loop over the generator that adds each square to total and adds 1 to count, giving total 18850, count 5 and the mean square 3770.0.
P01.10.3-F5

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 += 2

list(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.

The anchor figure again: during next(gen) the frame of countdown is on the call stack and running at yield n; between calls it waits, paused, inside the generator object, with n still bound to 3.
P01.10.3-F1

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 return the local variables are destroyed, while at a yield they 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

Your progress is saved in this browser only. No account, no tracking.
Back to top