Iterators and generators
The two-method protocol behind every for loop, generators that run only when asked, and the one-pass rule that silently empties a second loop.
After this lesson you can
- Tell an iterable from an iterator and drive one by hand with iter() and next()
- Write a generator function and predict when its body actually runs
- Recognise an exhausted iterator and avoid reusing one
- Use yield from and itertools.islice
Every for loop runs the same protocol. for x in thing first calls
iter(thing) to get an iterator, then calls next() on it until it
raises StopIteration, which the loop swallows and treats as "done".
- An iterable is anything with
__iter__: lists, dicts, strings, files, ranges. Each call toiter()on a list gives a new iterator starting from the beginning. - An iterator has
__next__and keeps a position. Its__iter__returns itself, so it can be used in aforloop too — but it only moves forwards, and once it has raisedStopIterationit stays finished.
def run(): nums = [10, 20] it = iter(nums) steps = [next(it), next(it)] try: next(it) except StopIteration: steps.append("StopIteration") return { "steps": steps, "iterator_is_its_own_iter": iter(it) is it, "list_gives_fresh_iterators": iter(nums) is not iter(nums), "after_exhaustion": list(it), "list_again": list(nums), }Generators
A function containing yield is a generator function. Calling it
runs none of its body; it returns a generator object, which is an
iterator. Each next() runs the body up to the next yield, hands that
value out, and freezes the function's local state until the following
next(). Falling off the end (or return) raises StopIteration.
def squares_below(limit):
n = 0
while n * n < limit:
yield n * n
n += 1
list(squares_below(20)) # [0, 1, 4, 9, 16]
That laziness is the point: a generator produces one value at a time, so
it can describe a sequence too large to hold in memory, or an infinite
one, and the consumer decides how much to take
(itertools.islice(gen, 5)). A generator expression is the
lazy form of a list comprehension: (x * x for x in nums) instead of
[x * x for x in nums]. yield from other yields everything other
produces, which keeps generators that combine other iterables short.
from itertools import islice log = [] def numbers(): n = 1 while True: # infinite, and that is fine log.append(f"make {n}") yield n n += 1 def chain(*iterables): for it in iterables: yield from it def run(): log.clear() gen = numbers() log.append("created") squares = (n * n for n in gen) for sq in islice(squares, 3): log.append(f"use {sq}") return {"log": log, "chained": list(chain([1, 2], "ab", range(3)))}One pass only
A generator, like any iterator, can be consumed once. Loop over it a
second time and the loop body simply never runs — no error, just
nothing. x in gen consumes items up to the match, and len(gen) does
not exist. If you need the values twice, keep a list, or call the
generator function again for a fresh generator.
def run(): squares = (n * n for n in range(5)) first = sum(squares) second = sum(squares) letters = iter("abcdef") found = "c" in letters remaining = "".join(letters) try: len(n for n in range(3)) length = "ok" except TypeError as e: length = f"TypeError: {e}" return {"first_sum": first, "second_sum": second, "found_c": found, "left_after_in": remaining, "len": length}Common mistakes
- Reusing an iterator. The second
sum()over the same generator returns0, andinsilently ate the items it searched past. - Expecting a generator function to do work when it is called. Nothing
runs until the first
next(); a validation check at the top of the body fires late, at the first iteration. - Treating a generator like a list:
len(), indexing and slicing all raiseTypeError. Uselist(gen)when you really need all of it.
What interviewers ask
- What is the difference between an iterable and an iterator? An
iterable hands out iterators through
__iter__; an iterator has__next__, keeps a position and is exhausted after one pass. - Why use a generator instead of returning a list? Memory and latency: values are produced on demand, the first one is available immediately, and infinite or huge sequences become possible.
- What happens when a generator function is called? It returns a
generator object without running the body;
next()runs it to the nextyield.
Exercise · Pro
Sliding windows, pulled lazily
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade