languages/generators-how-a-python-function-can-pause-in-the-middle-and-pick-up-w.md

Generators: how a Python function can pause in the middle and pick up where it left off

Programming languages · 4 min read ·

A generator is a Python function that can pause halfway, hand you a value, and carry on later from the same line with its variables intact.

Step through a real example one `next()` at a time, see why calling a generator function prints nothing at first, how a `for` loop knows when to stop, and why a million values can take about 200 bytes instead of 8 million. Includes the one catch that surprises beginners: a generator can only be used once.

Code lines with a yield line highlighted as the paused point, next to the title Python generators: pause, then resume

Most functions are like vending machines: you press a button, the whole thing runs, and one answer drops out. A generator is a function that can stop halfway, hand you a value, and wait. Later it carries on from the exact line where it stopped, with all its variables still in place.

Why would you want that? Because sometimes you need a million values but only look at them one at a time. Building all of them first wastes memory. A generator makes each one only when you ask.

The word that changes everything: yield

A normal function ends with return. When Python sees return, the function is finished and its local variables are thrown away. The keyword yield works differently: it hands a value out and pauses the function instead of ending it. Any function containing yield is a generator function.

def count_up(limit):
    print("  start")
    n = 1
    while n <= limit:
        print("  about to yield", n)
        yield n
        print("  resumed after", n)
        n += 1
    print("  finished")

g = count_up(2)
print("created, nothing printed yet")
print("next ->", next(g))
print("next ->", next(g))
try:
    next(g)
except StopIteration:
    print("StopIteration")

Running this with Python 3.13 prints:

created, nothing printed yet
  start
  about to yield 1
next -> 1
  resumed after 1
  about to yield 2
next -> 2
  resumed after 2
  finished
StopIteration

Look at the first line. Calling count_up(2) did not print "start". Calling a generator function runs none of its body. It gives you back a generator object (here stored in g), like a bookmark sitting before the first line. Only next(g) starts the work.

Try it: press next() and watch the pause

Notice what happens on the second press. The function does not start from the top. It resumes at print("resumed after", n), and n is still 1, because the pause kept the local variables alive. This is the whole trick: a generator is a function whose place and variables are saved between calls.

On the third press the loop ends, the function runs off its last line, and Python raises StopIteration. That is not a bug: it is the standard signal that says "no more values".

What a for loop does for you

You rarely call next() by hand. A for loop calls it repeatedly and quietly catches StopIteration to know when to stop:

def squares():
    for n in range(1, 4):
        yield n * n

for s in squares():
    print(s)
print(list(squares()))
1
4
9
[1, 4, 9]

Each call to squares() makes a fresh generator object, so the second use starts over from the beginning.

Why bother: memory

A list holds every value at once. A generator holds only its bookmark and variables. Compare a list of one million squares to the same thing written as a generator expression, which is a one-line generator written with round brackets:

import sys
big  = [n * n for n in range(1_000_000)]
lazy = (n * n for n in range(1_000_000))
print(sys.getsizeof(big))
print(sys.getsizeof(lazy))
8448728
200

The list takes about 8.4 million bytes for its own storage (the exact numbers vary by Python version and computer). The generator takes about 200 bytes, however many values it could produce. Nothing has been computed yet; lazy only knows how to compute the next one. A generator can even describe an endless sequence, such as doubling forever, because it never builds the whole thing:

def doubles():
    n = 1
    while True:
        yield n
        n *= 2

g = doubles()
print([next(g) for _ in range(5)])   # [1, 2, 4, 8, 16]

The catch: you can only go through it once

A generator remembers where it is, and it only moves forward. Once it is used up, it is empty, and it does not rewind:

lazy = (n * n for n in range(5))
print(list(lazy))
print(list(lazy))
[0, 1, 4, 9, 16]
[]

The second list(lazy) gives an empty list with no error, which surprises many beginners. If you need to go through the values twice, either make a new generator or store the values in a list.

So what, for your own code?

Reach for a generator when you process data one piece at a time and don't need all of it in memory: reading a big file line by line, producing numbers on demand, or building a pipeline where each step hands items to the next. If you need to count, index or loop over the values several times, a plain list is simpler and fine. Both are good tools; the generator's special power is waiting.

$ ls languages/

see all →
languages/javascript-s-event-loop-why-settimeout-0-runs-after-the-code-below-it.md JavaScript's event loop: why setTimeout(…, 0) runs after the code below it Why does `setTimeout(fn, 0)` run after the lines below it, and why does a promise callback beat it? JavaScript has one worker and two… 5 min read · 22 Oct languages/rust-ownership-why-let-b-a-makes-a-unusable.md Rust ownership: why let b = a makes a unusable Why does Rust refuse to compile `let b = a;` followed by a use of `a`? Because of ownership: every value has exactly one owner, and… 5 min read · 19 Oct languages/ruby-strings-why-changing-b-also-changes-a.md Ruby strings: why changing b also changes a In Ruby, `b = a` does not copy a string: it puts a second label on the same object. So `b << "s"` changes `a` too, while `b += "s"` does… 4 min read · 16 Oct languages/python-default-arguments-why-a-function-remembers-the-last-call.md Python default arguments: why a function remembers the last call Call a small Python function twice and the second call remembers the first. The cause is a default value like `basket=[]`, which Python… 4 min read · 13 Oct languages/go-slices-why-appending-to-a-copy-can-change-the-original.md Go slices: why appending to a copy can change the original Change a copy of a Go slice and sometimes the original changes too. The reason is that a slice is only a small window onto an array, with a… 4 min read · 10 Oct languages/why-does-5-3-give-53-in-javascript-type-coercion-step-by-step.md Why does "5" + 3 give "53" in JavaScript? Type coercion, step by step Why does `"5" + 3` give `"53"` while `"5" - 3` gives `2`? It is not random. JavaScript follows a short recipe called type coercion: it… 4 min read · 7 Oct