Learn Python / Iterators and Generators

Iterators and Generators

You've used for loops on lists, strings, dictionaries, files and range(). How does one kind of loop work with so many different things? The answer is iterators. Understanding them leads to one of Python's most useful features: generators, which produce values one at a time instead of building them all up front.

Iterables and iterators

Anything you can loop over is called an iterable: lists, strings, tuples, dictionaries, sets, files and so on.

An iterator is the object that actually walks through an iterable, handing out one item at a time. You get one with iter(), and ask it for the next item with next():

colors = ["red", "green", "blue"]

it = iter(colors)
print(next(it))
print(next(it))
print(next(it))
Output

When there are no items left, next() raises a StopIteration exception. You can catch it like any other exception:

it = iter(["only one"])
print(next(it))

try:
    next(it)
except StopIteration:
    print("StopIteration: no items left")
Output

What a for loop really does

A for loop is just a tidy way of doing exactly that. Behind the scenes:

  1. It calls iter() on what you're looping over.
  2. It calls next() over and over, running the body for each item.
  3. It stops quietly when StopIteration is raised.
word = "hey"

# what you write
for letter in word:
    print(letter)

# roughly what Python does for you
it = iter(word)
while True:
    try:
        letter = next(it)
    except StopIteration:
        break
    print(letter)
Output

An iterator can only be used once. Once it has handed out every item, it's empty:

numbers = iter([1, 2, 3])

print(list(numbers))
print(list(numbers))   # already used up
Output

Generators: functions that yield

A generator is the easiest way to make your own iterator. It looks like a normal function, but it uses yield instead of return:

def count_up_to(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1

for number in count_up_to(5):
    print(number)
Output

The difference is in when the code runs:

  • return sends back one value and the function is finished.
  • yield sends back a value and pauses the function. The next time a value is asked for, it carries on from exactly where it stopped, with all its variables intact.

Adding print calls makes the pausing visible:

def steps():
    print("  starting")
    yield 1
    print("  resumed after 1")
    yield 2
    print("  resumed after 2")
    yield 3
    print("  finished")

gen = steps()
print("got", next(gen))
print("got", next(gen))
print("got", next(gen))
Output

Calling steps() doesn't run any code yet. It just creates a generator object, and the body runs bit by bit as you ask for values.

Why use generators?

They save memory

A list holds every value at once. A generator produces each value only when it's needed, so it uses almost no memory, however many values it produces:

import sys

big_list = [n * n for n in range(1_000_000)]
big_gen = (n * n for n in range(1_000_000))

print(sys.getsizeof(big_list), "bytes for the list")
print(sys.getsizeof(big_gen), "bytes for the generator")
print(sum(big_gen))
Output

They can go on forever

A generator doesn't need an end. This one produces Fibonacci numbers endlessly, and the loop decides when to stop:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

for n in fibonacci():
    if n > 100:
        break
    print(n, end=" ")
Output

A list could never hold an endless sequence, but a generator can, because it only ever works out the next value.

Generator expressions

A generator expression looks like a list comprehension, but with round brackets instead of square ones:

squares_list = [n * n for n in range(5)]
squares_gen = (n * n for n in range(5))

print(squares_list)
print(squares_gen)
print(list(squares_gen))
Output

They're perfect for passing straight into functions like sum(), max() and any(). You can even drop the extra brackets:

prices = [4.99, 12.50, 3.25, 8.00]

print(round(sum(p * 1.2 for p in prices), 2))
print(max(len(w) for w in ["cat", "giraffe", "ox"]))
print(any(p > 10 for p in prices))
Output
Going deeper: Taking just a few items with itertools optional

The itertools module has tools for working with iterators. islice() takes the first few items from any iterator, even an endless one, and count() counts up forever:

from itertools import islice, count

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(islice(fibonacci(), 10)))
print(list(islice(count(100, 5), 4)))
Output
Going deeper: yield from optional

yield from hands out every item of another iterable, one by one. It's a shortcut for a for loop that yields each item:

def all_items(*groups):
    for group in groups:
        yield from group

print(list(all_items([1, 2], "ab", (True,))))
Output

Exercises

Exercise 1: Step through by hand

Use iter() and next() to print the first two items of the list, without a for loop. It should print Mon and then Tue.

days = ["Mon", "Tue", "Wed"]
Output

Exercise 2: Even numbers generator

Write a generator evens(limit) that yields the even numbers from 0 up to and including limit. The loop below should print 0 2 4 6 8 10.

def evens(limit):
    pass

for n in evens(10):
    print(n, end=" ")
Output

Exercise 3: Countdown generator

Write a generator countdown(n) that yields n, n - 1, … down to 1. Then print the list it produces for countdown(5): [5, 4, 3, 2, 1].

def countdown(n):
    pass
Output

Exercise 4: Sum with a generator expression

Use a single generator expression inside sum() to add up the lengths of all the words. It should print 23.

words = ["generators", "are", "really", "neat"]
Output

Iteration tools at a glance

A quick reference for later. The itertools examples need import itertools first.

Tool What it does Example Result
iter(items) Gets an iterator from any iterable iter([10, 20]) an iterator
next(it) The next item (StopIteration when there are none left) next(iter([10, 20])) 10
next(it, default) The next item, or default when there are none left next(iter([]), "done") 'done'
yield value Inside a function: hands out a value and pauses, making it a generator yield n * n one value per next()
yield from items Yields every item from another iterable yield from [1, 2] 1, then 2
(expr for x in items) A generator expression: produces items one at a time sum(x * x for x in range(4)) 14
itertools.count(start) Counts up forever list(itertools.islice(itertools.count(10), 3)) [10, 11, 12]
itertools.islice(it, n) Takes just the first n items list(itertools.islice("python", 2)) ['p', 'y']
itertools.cycle(items) Repeats the items forever list(itertools.islice(itertools.cycle("ab"), 5)) ['a', 'b', 'a', 'b', 'a']
itertools.chain(a, b) One iterable after another list(itertools.chain([1, 2], [3])) [1, 2, 3]
enumerate(items) Pairs each item with its position list(enumerate("ab")) [(0, 'a'), (1, 'b')]
zip(a, b) Walks several iterables side by side list(zip("ab", [1, 2])) [('a', 1), ('b', 2)]
reversed(items) Goes through a sequence backwards list(reversed([1, 2, 3])) [3, 2, 1]

Summary

  • An iterable is anything you can loop over. An iterator hands out its items one at a time.
  • iter() gets an iterator, next() gets the next item, and StopIteration means there are none left.
  • A for loop calls iter() and next() for you, and an iterator can only be used once.
  • A function with yield is a generator: it pauses at each yield and resumes on the next request.
  • Generators save memory and can even be endless.
  • (expr for x in items) is a generator expression. It's perfect inside sum(), max() and any().