Skip to content
FollowtheDamnCode

Iterators and Generators

yield, laziness, and how for loops really work.

5 minute read

Big idea

An iterator hands out values one at a time. A generator is the easiest way to make one: a function that uses yield to produce each value only when it's asked for.

How a for loop really works

Anything you can loop over is iterable. Behind the scenes, for asks it for an iterator, then calls next() on that until it runs out:

What for does behind the scenes
letters = iter("abc")
print(next(letters))
print(next(letters))
print(next(letters))
output
a
b
c

Run and edit it in the app.

One more next(letters) would raise StopIteration — the signal a for loop uses to stop.

yield

A function with yield in it doesn't run when you call it. It returns a generator, which runs the body only as far as the next yield, hands out that value, and pauses there:

Predict: when does each line run?
def countdown(n):
    print("starting")
    while n > 0:
        yield n
        n -= 1
    print("finished")

gen = countdown(3)
print("made the generator")
for value in gen:
    print(value)
What does it print? Show the answer
made the generator
starting
3
2
1
finished

Run and edit it in the app.

"starting" only appears once the loop asks for the first value. Each yield pauses the function with all its variables intact; the next request resumes right after it.

A generator pauses at each yield

Why bother? Laziness

A generator never builds the whole sequence. It can even be infinite, because only the values you ask for are ever made:

An endless sequence, taken five at a time
def evens():
    n = 0
    while True:
        yield n
        n += 2

for i, value in enumerate(evens()):
    if i == 5:
        break
    print(value)
output
0
2
4
6
8

Run and edit it in the app.

A list of every even number would never finish building. The generator makes exactly five.

Generator expressions

Swap a comprehension's square brackets for round ones and you get a generator instead of a list:

total = sum(n * n for n in range(1_000_000))   # no million-item list in memory

Used up after one pass

Fix the bug: the sum should be 14, not 0
squares = (n * n for n in range(4))
print(list(squares))
print(sum(squares))
output
[0, 1, 4, 9]
14

Run and edit it in the app.

A generator is used up after one pass — the second use finds it empty, so sum is 0. Make a list once (squares = [n * n for n in range(4)]) if you need the values twice.

What happens when you call a function that contains yield?

Watch it

Think of it like

A Pez dispenser versus a bag of sweets. The bag (a list) holds everything at once. The dispenser (a generator) gives you one sweet each time you tilt it — and it never had to make them all up front.

Key terms

  • Iterator — An object that gives the next value each time next() is called.
  • Generator — A function using yield; produces values lazily.
  • Lazy evaluation — Computing values only when they're needed.

Check yourself

What does yield do?

Why can a generator be infinite?

What is (n * n for n in range(5))?