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))
The iterator remembers its position: each next(it) gives the item after the last one, so you get red, then green, then blue. The list itself doesn't change. You can't call next() on the list directly; next(colors) is a TypeError, because a list isn't an iterator. iter() makes one for it.
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")What a for loop really does
A for loop is just a tidy way of doing exactly that. Behind the scenes:
- It calls
iter()on what you're looping over. - It calls
next()over and over, running the body for each item. - It stops quietly when
StopIterationis 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)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)) # used up
The second line prints []. So why can you loop over the same list as many times as you like? Because each for loop calls iter() again and gets a fresh iterator. The list holds the data; the iterator is just a bookmark.
| Iterable (e.g. a list) | Iterator (e.g. iter(list)) |
|
|---|---|---|
| What it is | something you can loop over | the thing that walks through it |
next() works? |
no | yes |
| Loop over it twice? | yes: each loop gets a new iterator | no: it's used up after one pass |
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)The loop asks the generator for a value, and the function runs until it reaches yield n, which hands out 1 and pauses. The next request carries on after the yield: n += 1, back to the while check, and yield again with 2. When n reaches 6, the while loop ends, the function reaches its end, and that tells the for loop to stop (Python raises StopIteration for you).
The difference is in when the code runs:
returnsends back one value and the function is finished.yieldsends 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))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. Notice that starting only appears with the first next(), and finished never appears at all: nobody asked for a fourth value, so the function stayed paused after yield 3. A fourth next(gen) would print finished and then raise StopIteration.
A generator is an iterator, so it can only be used once. To go through the values again, call the function again to get a new generator.
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))
The list needs megabytes, because all million squares exist at once. The generator needs only a couple of hundred bytes, because it holds just its current position. sum(big_gen) still gets every value, one at a time. The exact numbers depend on your Python version. The trade-off: a list lets you index it (big_list[10]) and go through it again, and a generator doesn't.
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=" ")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))
Printing the generator shows only <generator object ...>, because no values have been worked out yet. list() asks for all of them. Like any generator, it's used up afterwards, so a second list(squares_gen) gives [].
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))
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)))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,))))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"]
iter(days) gives you an iterator over the list. Store it in a variable.
Each call to next(it) gives the next item. Print it twice.
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=" ")Start with n = 0 and loop while n <= limit:.
Inside the loop, yield n and then add 2 to n.
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
Loop while n > 0:, yield n, then subtract 1.
list(countdown(5)) collects everything the generator yields into a list.
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"]
A generator expression looks like a comprehension: len(w) for w in words.
Put it straight inside sum(...), without square brackets.
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, andStopIterationmeans there are none left.- A
forloop callsiter()andnext()for you, and an iterator can only be used once. - A function with
yieldis a generator: it pauses at eachyieldand 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 insidesum(),max()andany().
Know something worth teaching? Write your own tutorial with runnable examples and share it.