12.1. Iterators#
Learning goals — By the end of this section you will be able to:
Describe Python’s iterator protocol and distinguish iterables from iterators
Use
iter()andnext()manually to step through any iterableUse lazy built-in iterators:
enumerate(),zip(),map(),filter(),reversed()Implement a custom iterator class with
__iter__and__next__Raise
StopIterationcorrectly to signal the end of a sequence
12.1.1. The Iterator Protocol#
Any object you can loop over is an iterable. Under the hood, for loops call two methods:
Method |
Called on |
Returns |
|---|---|---|
|
the iterable |
an iterator object |
|
the iterator |
the next value, or raises |
An object that implements both methods is its own iterator (e.g., file objects, generators). A list is iterable but not its own iterator — iter(my_list) creates a separate iterator object.
# iter() creates an iterator from any iterable
my_list = [10, 20, 30]
it = iter(my_list)
print(type(it)) # <class 'list_iterator'>
# next() fetches values one at a time
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# The for loop does this for you automatically:
# for item in my_list: → it = iter(my_list); next(it); next(it); ...
# Calling next() past the end raises StopIteration
try:
print(next(it))
except StopIteration:
print("StopIteration raised — sequence exhausted")
<class 'list_iterator'>
10
20
30
StopIteration raised — sequence exhausted
12.1.2. Built-in Lazy Iterators#
Python’s standard library is full of iterators that produce values on demand — they don’t build a list in memory first. This makes them memory-efficient for large or infinite sequences.
Iterator |
What it produces |
|---|---|
|
|
|
|
|
|
|
items where |
|
items in reverse order |
fruits = ['apple', 'banana', 'cherry']
# enumerate — index + value pairs
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# zip — pair two sequences together
prices = [1.20, 0.50, 2.00]
for fruit, price in zip(fruits, prices):
print(f"{fruit}: ${price:.2f}")
# map and filter return iterators (wrap in list() to inspect)
lengths = list(map(len, fruits)) # [5, 6, 6]
long_ones = list(filter(lambda f: len(f) > 5, fruits)) # ['banana', 'cherry']
print(lengths)
print(long_ones)
# reversed
for fruit in reversed(fruits):
print(fruit, end=' ')
print()
1. apple
2. banana
3. cherry
apple: $1.20
banana: $0.50
cherry: $2.00
[5, 6, 6]
['banana', 'cherry']
cherry banana apple
12.1.3. Custom Iterator Classes#
When you need iteration logic that doesn’t map cleanly to a built-in or generator, implement the iterator protocol directly. Your class needs two methods:
__iter__(self)— returnsself(the iterator is its own iterable)__next__(self)— returns the next value, or raisesStopIteration
class Countdown:
"""Counts down from `start` to 1, then raises StopIteration."""
def __init__(self, start):
self.current = start
def __iter__(self):
return self # the iterator IS the object
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Works in a for loop
for n in Countdown(5):
print(n, end=' ')
print() # 5 4 3 2 1
# Also works with list(), sum(), max(), etc.
print(list(Countdown(4))) # [4, 3, 2, 1]
print(sum(Countdown(10))) # 55
5 4 3 2 1
[4, 3, 2, 1]
55
class NumberRange:
"""Iterates from `start` to `stop` (exclusive) by `step`."""
def __init__(self, start, stop, step=1):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if (self.step > 0 and self.current >= self.stop) or \
(self.step < 0 and self.current <= self.stop):
raise StopIteration
value = self.current
self.current += self.step
return value
print(list(NumberRange(1, 10, 2))) # [1, 3, 5, 7, 9]
print(list(NumberRange(10, 0, -3))) # [10, 7, 4, 1]
[1, 3, 5, 7, 9]
[10, 7, 4, 1]
### Exercise: Cycling Iterator
# Implement a `Cycle` iterator that repeatedly loops through a sequence forever.
# Example: Cycle(['R', 'G', 'B']) yields R, G, B, R, G, B, R, ...
# 1. Store the sequence and the current index.
# 2. `__next__` returns the item at the current index and advances; wraps with modulo.
# 3. Test: collect the first 7 values from Cycle([1, 2, 3]) → [1, 2, 3, 1, 2, 3, 1].
# Hint: use `import itertools; list(itertools.islice(your_cycle, 7))` to cap it.
### Your code starts here.
### Your code ends here.
### Solution
import itertools
class Cycle:
def __init__(self, sequence):
self.sequence = list(sequence)
self.index = 0
def __iter__(self):
return self
def __next__(self):
if not self.sequence:
raise StopIteration
value = self.sequence[self.index]
self.index = (self.index + 1) % len(self.sequence)
return value
# Collect the first 7 values
print(list(itertools.islice(Cycle([1, 2, 3]), 7))) # [1, 2, 3, 1, 2, 3, 1]
print(list(itertools.islice(Cycle(['R', 'G', 'B']), 8))) # ['R', 'G', 'B', ...]
[1, 2, 3, 1, 2, 3, 1]
['R', 'G', 'B', 'R', 'G', 'B', 'R', 'G']
12.1.4. Summary#
Concept |
Key idea |
|---|---|
Iterable |
Has |
Iterator |
Has |
|
Returns an iterator from any iterable |
|
Returns the next value from an iterator |
|
Yields |
|
Pairs items from two iterables, stops at shortest |
Custom iterator |
Class with |