Why this matters
Writing an iterator class by hand, as in the last lesson, takes a dozen lines of boilerplate to remember a position. Generators let you write the same thing as an ordinary-looking function with yield in it, and Python builds the iterator for you. Because generators produce values lazily, one at a time on demand, they let you process files larger than memory, model infinite sequences, and chain processing stages into pipelines where each item flows through all stages before the next item is even read. Much of the standard library (and well-written data-processing code) is built on them.
yield turns a function into a generator
def countdown(n):
while n > 0:
yield n
n -= 1
for x in countdown(3):
print(x)
print(list(countdown(5)))
3
2
1
[5, 4, 3, 2, 1]
Compare this with the Countdown class from last lesson: same behavior, a third of the code. Any function containing yield (anywhere in its body) is a generator function. Calling it does not run the body. It returns a generator object, which is an iterator:
g = countdown(2)
print(g)
print(next(g))
print(next(g))
print(next(g))
<generator object countdown at 0x7f...>
2
1
Traceback (most recent call last):
...
StopIteration
What happens underneath: a suspended frame
When a normal function is called, Python creates a frame: the object holding its local variables and the position of the instruction being executed. When the function returns, the frame is discarded.
A generator keeps its frame alive between values:
g = countdown(2) frame created, paused before line 1 (nothing printed)
next(g) runs until "yield n" with n=2 returns 2, frame paused
next(g) resumes after the yield: n -= 1,
loops, hits "yield n" with n=1 returns 1, frame paused
next(g) resumes: n -= 1, loop ends,
function body finishes raises StopIteration
yield is like return that does not destroy the frame: local variables, the loop position, everything stays as it was until the next next(). A return statement (or falling off the end) in a generator raises StopIteration to signal the end. You can see the paused frame in the REPL with g.gi_frame.f_locals and g.gi_frame.f_lineno.
This is why the body runs lazily:
def noisy():
print("start")
yield 1
print("middle")
yield 2
print("end")
g = noisy()
print("created")
for v in g:
print("got", v)
created
start
got 1
middle
got 2
end
Laziness saves memory
A list holds all its elements at once; a generator holds only its current state. Compare:
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print(sys.getsizeof(squares_list) > 8_000_000, sys.getsizeof(squares_gen) < 500)
print(sum(squares_gen))
True True
333332833333500000
The list needs about 8 MB just for its pointers (plus the int objects); the generator is a couple of hundred bytes, whatever the range. That is the difference between processing a 20 GB log file line by line and running out of memory.
Generator expressions
The (n * n for n in range(10)) above is a generator expression: comprehension syntax with parentheses, producing a generator instead of a list. When it is the only argument of a function, you can drop the extra parentheses:
words = ["apple", "kiwi", "banana"]
print(sum(len(w) for w in words))
print(max(len(w) for w in words))
print(any(w.startswith("k") for w in words))
15
6
True
Use a list comprehension when you need the list (indexing, several passes, len); use a generator expression when you consume the values once.
Infinite generators
A generator may never finish; the consumer decides when to stop:
import itertools
def naturals():
n = 1
while True:
yield n
n += 1
evens = (n for n in naturals() if n % 2 == 0)
print(list(itertools.islice(evens, 5)))
[2, 4, 6, 8, 10]
Each value is computed only when islice asks for it, so the infinite loop costs nothing beyond what is used.
Pipelines
Generators compose. Each stage takes an iterable and yields transformed items:
def read(lines):
for line in lines:
line = line.strip()
if line and not line.startswith("#"):
yield line
def to_ints(lines):
for line in lines:
yield int(line)
def running_total(nums):
total = 0
for n in nums:
total += n
yield total
data = ["# header", "3", "", "4", "10"]
print(list(running_total(to_ints(read(data)))))
[3, 7, 17]
Data flows through one item at a time: the first line goes through read, to_ints and running_total before the second line is touched. Pass sys.stdin or an open file instead of data and the same code processes any amount of input in constant memory. This is the Unix pipe philosophy inside one program.
yield from
A generator can delegate to another iterable with yield from:
def chain(*iterables):
for it in iterables:
yield from it
print(list(chain([1, 2], "ab", range(3))))
[1, 2, 'a', 'b', 0, 1, 2]
yield from it is roughly for x in it: yield x, and it also forwards values sent into the generator and the sub-generator's return value (features used by older coroutine code). It shines in recursive generators such as walking trees or nested lists.
Note: Generators also support
send(),throw()andclose(), turning them into coroutines that receive values. That machinery was the foundation ofasyncio, covered in the concurrency module; for everyday code,yieldandyield fromare what you need.
Common mistakes
- Expecting the body to run on the call:
g = gen()runs nothing; side effects happen only as you iterate. - Iterating a generator twice: generators are iterators, so they are one-shot. The second loop yields nothing. Call the generator function again for a fresh one.
- Using
return valueto produce the result: in a generator,return xends iteration (the value is attached toStopIterationand normally ignored). Useyield. len()of a generator:TypeError: object of type 'generator' has no len(). Generators do not know how many values they will produce.list()on an infinite generator: the program runs until it is killed for memory or time. Useisliceor abreak.
Recap
- A function containing
yieldreturns a generator object when called; its body runs lazily, oneyieldat a time, on eachnext(). - The generator keeps its frame (locals and position) suspended between values; finishing raises
StopIteration. - Generator expressions
(expr for x in it)are lazy comprehensions, ideal for one-pass consumers likesumandany. - Chained generators form memory-efficient pipelines;
yield fromdelegates to a sub-iterable.
Going further: explore the itertools module documentation, especially its "Itertools Recipes" section, which is a masterclass in generator pipelines. PEP 255 introduced generators; PEP 380 introduced yield from.