Iterators & Generators

Every for loop in Python runs on the iterator protocol. Generators — functions that yield values one at a time — make it easy to create your own iterators, process data lazily, and handle streams far larger than memory.

Iterables and iterators

An iterable is anything you can loop over (lists, strings, dictionaries, files). Calling iter() on it gives an iterator, which hands out one item per call to next() and raises StopIteration when it runs out:

PY protocol.py
colors = ["red", "green"]
it = iter(colors)

print(next(it))
print(next(it))
print(next(it, "no more items"))
Output
red
green
no more items

A for loop does exactly this behind the scenes: call iter(), then next() repeatedly until StopIteration.

Generator functions

A function containing yield becomes a generator function. Calling it doesn't run the body; it returns a generator, which runs the body step by step, pausing at each yield:

PY generator.py
def countdown(n):
    print("starting")
    while n > 0:
        yield n
        n -= 1
    print("done")

gen = countdown(3)
print(type(gen).__name__)
print(next(gen))
print(next(gen))
print(list(gen))
Output
generator
starting
3
2
done
[1]

"starting" only prints when the first value is requested, and the function's local variables are preserved between yields.

Lazy evaluation

Generators produce values on demand, so they use almost no memory regardless of how many values there are — and can even be infinite:

PY lazy.py
import itertools

def squares():
    n = 1
    while True:
        yield n * n
        n += 1

first_ten = list(itertools.islice(squares(), 10))
print(first_ten)

big = (n * n for n in range(10_000_000))
print(next(big), next(big))
Output
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
0 1

The generator expression on the last lines would describe ten million squares, but only the two requested are ever computed.

Pipelines

Chaining generators creates efficient data pipelines, where each item flows through every step before the next item is read:

PY pipeline.py
log = """INFO start
ERROR disk full
INFO retry
ERROR timeout
WARN slow"""

def read_lines(text):
    for line in text.splitlines():
        yield line

def errors_only(lines):
    for line in lines:
        if line.startswith("ERROR"):
            yield line

def messages(lines):
    for line in lines:
        yield line.split(" ", 1)[1]

for msg in messages(errors_only(read_lines(log))):
    print(msg)
Output
disk full
timeout

With a real log file, read_lines would read one line at a time — the whole pipeline would handle gigabytes with tiny memory use.

yield from

PY yield_from.py
def walk(tree):
    yield tree["name"]
    for child in tree.get("children", []):
        yield from walk(child)

site = {"name": "home", "children": [{"name": "blog", "children": [{"name": "post-1"}]}, {"name": "about"}]}
print(list(walk(site)))
Output
['home', 'blog', 'post-1', 'about']

yield from passes along every value from another iterable — ideal for recursive structures.

Generators are single-use

PY single_use.py
gen = (n for n in range(3))
print(list(gen))
print(list(gen))
Output
[0, 1, 2]
[]

Once consumed, a generator is empty. If you need the values twice, store them in a list or create a new generator.

Writing an iterator class

For full control, a class can implement __iter__ and __next__. Often, though, making __iter__ a generator is simpler:

PY iter_class.py
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

timer = Countdown(3)
print(list(timer), list(timer))
Output
[3, 2, 1] [3, 2, 1]

Because __iter__ creates a fresh generator each time, a Countdown object can be looped over repeatedly.

Built-ins that work with iterators

PY builtins.py
names = ["ana", "ben", "cleo"]
scores = [91, 78, 85]

print(list(enumerate(names, 1)))
print(list(zip(names, scores)))
print(list(map(str.title, names)))
print(sum(s for s in scores if s > 80))
print(sorted(zip(scores, names), reverse=True)[0])
Output
[(1, 'ana'), (2, 'ben'), (3, 'cleo')]
[('ana', 91), ('ben', 78), ('cleo', 85)]
['Ana', 'Ben', 'Cleo']
176
(91, 'ana')

enumerate, zip, map, filter, reversed and generator expressions all return lazy iterators. The itertools module (lesson 51) provides many more.

Try it yourself

Try it yourself
Console output