Iterators & Generators
Iterator Protocol
When you write for item in my_object, Python needs to know how to get items from your object. The iterator protocol defines this contract - implement iter and next and your custom classes work seamlessly with for loops, list(), and all iteration tools.
The iterator protocol in Python defines how objects can be iterated over. Any object that implements __iter__() and __next__() is an iterator.
Basic Iterator
basic.py
Replay: real traced execution (multi-file project)
# Basic iterator
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Use the iterator
counter = Counter(1, 5)
for num in counter:
print(num)
# Basic iterator
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Use the iterator
counter = Counter(0, 3)
for num in counter:
print(num)
# Basic iterator
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Use the iterator
counter = Counter(2, 7)
for num in counter:
print(num)
counter = Counter(1, 5)
20# Use the iterator21counter = Counter(1, 5)22#@counter=Counter(0, 3), Counter(2, 7)self.current ← 1, self.end ← 5
4class Counter:5 def __init__(self⟨Counter A⟩, start1, end5):6 self.current→ 1 = start17 self.end→ 5 = end5counter ← ⟨Counter A⟩
20# Use the iterator21counter→ ⟨Counter A⟩ = Counter(1, 5)22#@counter=Counter(0, 3), Counter(2, 7)def __iter__(self):
9def __iter__(self⟨Counter A⟩):10 return selfvalue ← 1, self.current ← 2
pass 1 of 512def __next__(self⟨Counter A⟩):13 if self.current >= self.end:14 raise StopIteration15 value→ 1 = self.current116 self.current→ 2 += 117 return value1All 5 passes — pass 1 is the card above pass self.endStopIterationvalueself.current1 — — 1 1 → 2 2 — — 2 2 → 3 3 — — 3 3 → 4 4 — — 4 4 → 5 5 5 <class 'StopIteration'> — 5 for num in counter:
pass 1 of 422#@counter=Counter(0, 3), Counter(2, 7)23for num1 in counter⟨Counter A⟩:24 print(num1)output1All 4 passes — pass 1 is the card above pass numself.currentself.endStopIteration1 1 — — — 2 2 — — — 3 3 — — — 4 4 5 5 <class 'StopIteration'> if self.current >= self.end:
12def __next__(self):13 if self.current5 >= self.end5:14 raise StopIteration<class 'StopIteration'>15 value = self.current
counter = Counter(0, 3)
20# Use the iterator21counter = Counter(0, 3)22for num in counter:self.current ← 0, self.end ← 3
4class Counter:5 def __init__(self⟨Counter A⟩, start0, end3):6 self.current→ 0 = start07 self.end→ 3 = end3counter ← ⟨Counter A⟩
20# Use the iterator21counter→ ⟨Counter A⟩ = Counter(0, 3)22for num in counter:def __iter__(self):
9def __iter__(self⟨Counter A⟩):10 return selfvalue ← 0, self.current ← 1
pass 1 of 412def __next__(self⟨Counter A⟩):13 if self.current >= self.end:14 raise StopIteration15 value→ 0 = self.current016 self.current→ 1 += 117 return value0All 4 passes — pass 1 is the card above pass self.endStopIterationvalueself.current1 — — 0 0 → 1 2 — — 1 1 → 2 3 — — 2 2 → 3 4 3 <class 'StopIteration'> — 3 for num in counter:
pass 1 of 321counter = Counter(0, 3)22for num0 in counter⟨Counter A⟩:23 print(num0)output0All 3 passes — pass 1 is the card above pass numself.currentself.endStopIteration1 0 — — — 2 1 — — — 3 2 3 3 <class 'StopIteration'> if self.current >= self.end:
12def __next__(self):13 if self.current3 >= self.end3:14 raise StopIteration<class 'StopIteration'>15 value = self.current
counter = Counter(2, 7)
20# Use the iterator21counter = Counter(2, 7)22for num in counter:self.current ← 2, self.end ← 7
4class Counter:5 def __init__(self⟨Counter A⟩, start2, end7):6 self.current→ 2 = start27 self.end→ 7 = end7counter ← ⟨Counter A⟩
20# Use the iterator21counter→ ⟨Counter A⟩ = Counter(2, 7)22for num in counter:def __iter__(self):
9def __iter__(self⟨Counter A⟩):10 return selfvalue ← 2, self.current ← 3
pass 1 of 612def __next__(self⟨Counter A⟩):13 if self.current >= self.end:14 raise StopIteration15 value→ 2 = self.current216 self.current→ 3 += 117 return value2All 6 passes — pass 1 is the card above pass self.endStopIterationvalueself.current1 — — 2 2 → 3 2 — — 3 3 → 4 3 — — 4 4 → 5 4 — — 5 5 → 6 5 — — 6 6 → 7 6 7 <class 'StopIteration'> — 7 for num in counter:
pass 1 of 521counter = Counter(2, 7)22for num2 in counter⟨Counter A⟩:23 print(num2)output2All 5 passes — pass 1 is the card above pass numself.currentself.endStopIteration1 2 — — — 2 3 — — — 3 4 — — — 4 5 — — — 5 6 7 7 <class 'StopIteration'> if self.current >= self.end:
12def __next__(self):13 if self.current7 >= self.end7:14 raise StopIteration<class 'StopIteration'>15 value = self.current
iterator protocol - the __iter__() and __next__() methods that let objects work with for loops
How for Loops Work
manual.py
Replay: real traced execution (multi-file project)
# Using iter() and next()
data = [10, 20, 30]
# Get iterator manually
it = iter(data) # calls data.__iter__()
# Get values manually
print(next(it)) # calls it.__next__() -> 10
print(next(it)) # -> 20
print(next(it)) # -> 30
# StopIteration on exhaustion
try:
print(next(it)) # iterator exhausted
except StopIteration:
print("Iterator exhausted")
data ← [10, 20, 30], it ← ⟨list_iterator A⟩
4data→ [10, 20, 30] = [10, 20, 30]56# Get iterator manually7it→ ⟨list_iterator A⟩ = iter(data[10, 20, 30]) # calls data.__iter__()89# Get values manually10print(next(it⟨list_iterator A⟩)) # calls it.__next__() -> 1011print(next(it⟨list_iterator A⟩)) # -> 2012print(next(it⟨list_iterator A⟩)) # -> 30output10 20 30try:
14# StopIteration on exhaustion15try:16 print(next(it⟨list_iterator A⟩)) # iterator exhausted17except StopIteration:except StopIteration:
16 print(next(it)) # iterator exhausted17except StopIteration:18 print("Iterator exhausted")outputIterator exhausted
The for loop:
for item in iterable:
process(item)
Is equivalent to:
iterator = iter(iterable) # calls __iter__()
while True:
try:
item = next(iterator) # calls __next__()
process(item)
except StopIteration:
break
Iterable vs Iterator
iterable_vs_iterator.py
Replay: real traced execution (multi-file project)
# Iterable vs iterator
class NumberList:
"""Iterable (not an iterator)"""
def __init__(self, numbers):
self.numbers = numbers
def __iter__(self):
return NumberIterator(self.numbers)
class NumberIterator:
"""Iterator for NumberList"""
def __init__(self, numbers):
self.numbers = numbers
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.numbers):
raise StopIteration
value = self.numbers[self.index]
self.index += 1
return value
# Use the iterable multiple times
numbers = NumberList([10, 20, 30])
print("First iteration:")
for n in numbers:
print(n)
print("\nSecond iteration:")
for n in numbers:
print(n)
"""Iterable (not an iterator)"""
4class NumberList:5 """Iterable (not an iterator)"""67 def __init__(self, numbers):8 self.numbers = numbers910 def __iter__(self):11 return NumberIterator(self.numbers)121314class NumberIterator:15 """Iterator for NumberList"""1617 def __init__(self, numbers):18 self.numbers = numbers19 self.index = 02021 def __iter__(self):22 return self2324 def __next__(self):25 if self.index >= len(self.numbers):26 raise StopIteration27 value = self.numbers[self.index]28 self.index += 129 return value303132# Use the iterable multiple times33numbers = NumberList([10, 20, 30])self.numbers ← [10, 20, 30]
7def __init__(self⟨NumberList A⟩, numbers[10, 20, 30]):8 self.numbers→ [10, 20, 30] = numbers[10, 20, 30]numbers ← ⟨NumberList A⟩
32# Use the iterable multiple times33numbers→ ⟨NumberList A⟩ = NumberList([10, 20, 30])3435print("First iteration:")36for n in numbers:outputFirst iteration:def __iter__(self):
pass 1 of 210def __iter__(self⟨NumberList A⟩):11 return NumberIterator(self.numbers[10, 20, 30])self.numbers ← [10, 20, 30], self.index ← 0
pass 1 of 217def __init__(self⟨NumberIterator B⟩, numbers[10, 20, 30]):18 self.numbers→ [10, 20, 30] = numbers[10, 20, 30]19 self.index→ 0 = 0value ← 10, self.index ← 1
pass 1 of 824def __next__(self⟨NumberIterator B⟩):25 if self.index >= len(self.numbers):26 raise StopIteration27 value→ 10 = self.numbers[self.index]1028 self.index→ 1 += 129 return value10All 8 passes — pass 1 is the card above pass selfself.numbers[self.index]self.numbersStopIterationvalueself.index1 ⟨NumberIterator B⟩ 10 — — 10 0 → 1 2 ⟨NumberIterator B⟩ 20 — — 20 1 → 2 3 ⟨NumberIterator B⟩ 30 — — 30 2 → 3 4 ⟨NumberIterator B⟩ — [10, 20, 30] <class 'StopIteration'> — 3 5 ⟨NumberIterator C⟩ 10 — — 10 0 → 1 6 ⟨NumberIterator C⟩ 20 — — 20 1 → 2 7 ⟨NumberIterator C⟩ 30 — — 30 2 → 3 8 ⟨NumberIterator C⟩ — [10, 20, 30] <class 'StopIteration'> — 3 for n in numbers:
pass 1 of 335print("First iteration:")36for n10 in numbers⟨NumberList A⟩:37 print(n10)output10All 3 passes — pass 1 is the card above pass nself.indexself.numbersStopIteration1 10 — — — 2 20 — — — 3 30 3 [10, 20, 30] <class 'StopIteration'> if self.index >= len(self.numbers):
pass 1 of 224def __next__(self):25 if self.index3 >= len(self.numbers[10, 20, 30]):26 raise StopIteration<class 'StopIteration'>27 value = self.numbers[self.index]print(" Second iteration:")
39print("\nSecond iteration:")40for n in numbers:output Second iteration:def __iter__(self):
pass 2 of 210def __iter__(self⟨NumberList A⟩):11 return NumberIterator(self.numbers[10, 20, 30])self.numbers ← [10, 20, 30], self.index ← 0
pass 2 of 217def __init__(self⟨NumberIterator C⟩, numbers[10, 20, 30]):18 self.numbers→ [10, 20, 30] = numbers[10, 20, 30]19 self.index→ 0 = 0for n in numbers:
pass 1 of 339print("\nSecond iteration:")40for n10 in numbers⟨NumberList A⟩:41 print(n10)output10All 3 passes — pass 1 is the card above pass nself.indexself.numbersStopIteration1 10 — — — 2 20 — — — 3 30 3 [10, 20, 30] <class 'StopIteration'> if self.index >= len(self.numbers):
pass 2 of 224def __next__(self):25 if self.index3 >= len(self.numbers[10, 20, 30]):26 raise StopIteration<class 'StopIteration'>27 value = self.numbers[self.index]
iterable - an object with __iter__() that returns an iterator; can be iterated multiple times
StopIteration - the exception that signals the end of iteration
Custom Range Implementation
custom_range.py
Replay: real traced execution (multi-file project)
# Custom range-like iterator
class MyRange:
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
# Use like built-in range
print("Forward:")
for i in MyRange(0, 10, 2):
print(i, end=" ")
print("\n\nBackward:")
for i in MyRange(10, 0, -2):
print(i, end=" ")
print("Forward:")
23# Use like built-in range24print("Forward:")25for i in MyRange(0, 10, 2):outputForward:self.current ← 0, self.stop ← 10, self.step ← 2
pass 1 of 24class MyRange:5 def __init__(self⟨MyRange A⟩, start0, stop10, step2=1):6 self.current→ 0 = start07 self.stop→ 10 = stop108 self.step→ 2 = step2def __iter__(self):
pass 1 of 210def __iter__(self⟨MyRange A⟩):11 return selfvalue ← 0, self.current ← 2
pass 1 of 1213def __next__(self⟨MyRange A⟩):14 if (self.step > 0 and self.current >= self.stop) or (15 self.step < 0 and self.current <= self.stop16 ):17 raise StopIteration18 value→ 0 = self.current019 self.current→ 2 += self.step220 return value0All 12 passes — pass 1 is the card above pass selfself.stepself.stopStopIterationvalueself.current1 ⟨MyRange A⟩ 2 — — 0 0 → 2 2 ⟨MyRange A⟩ 2 — — 2 2 → 4 3 ⟨MyRange A⟩ 2 — — 4 4 → 6 4 ⟨MyRange A⟩ 2 — — 6 6 → 8 5 ⟨MyRange A⟩ 2 — — 8 8 → 10 6 ⟨MyRange A⟩ 2 10 <class 'StopIteration'> — 10 7 ⟨MyRange B⟩ -2 — — 10 10 → 8 8 ⟨MyRange B⟩ -2 — — 8 8 → 6 9 ⟨MyRange B⟩ -2 — — 6 6 → 4 10 ⟨MyRange B⟩ -2 — — 4 4 → 2 11 ⟨MyRange B⟩ -2 — — 2 2 → 0 12 ⟨MyRange B⟩ -2 0 <class 'StopIteration'> — 0 for i in MyRange(0, 10, 2):
pass 1 of 524print("Forward:")25for i0 in MyRange(0, 10, 2):26 print(i0, end=" ")output0All 5 passes — pass 1 is the card above pass iself.stepself.currentself.stopStopIteration1 0 — — — — 2 2 — — — — 3 4 — — — — 4 6 — — — — 5 8 2 10 10 <class 'StopIteration'> if (self.step > 0 and self.current >= self.stop) or ( self…
pass 1 of 213def __next__(self):14 if (self.step2 > 0 and self.current10 >= self.stop10) or (15 self.step2 < 0 and self.current10 <= self.stop1016 ):17 raise StopIteration<class 'StopIteration'>18 value = self.currentprint(" Backward:")
28print("\n\nBackward:")29for i in MyRange(10, 0, -2):output Backward:self.current ← 10, self.stop ← 0, self.step ← -2
pass 2 of 24class MyRange:5 def __init__(self⟨MyRange B⟩, start10, stop0, step-2=1):6 self.current→ 10 = start107 self.stop→ 0 = stop08 self.step→ -2 = step-2def __iter__(self):
pass 2 of 210def __iter__(self⟨MyRange B⟩):11 return selffor i in MyRange(10, 0, -2):
pass 1 of 528print("\n\nBackward:")29for i10 in MyRange(10, 0, -2):30 print(i10, end=" ")output10All 5 passes — pass 1 is the card above pass iself.stepself.currentself.stopStopIteration1 10 — — — — 2 8 — — — — 3 6 — — — — 4 4 — — — — 5 2 -2 0 0 <class 'StopIteration'> if (self.step > 0 and self.current >= self.stop) or ( self…
pass 2 of 213def __next__(self):14 if (self.step-2 > 0 and self.current0 >= self.stop0) or (15 self.step-2 < 0 and self.current0 <= self.stop016 ):17 raise StopIteration<class 'StopIteration'>18 value = self.current
Infinite Iterators
infinite.py
Replay: real traced execution (multi-file project)
# Infinite iterator
class InfiniteCounter:
def __init__(self, start=0):
self.current = start
def __iter__(self):
return self
def __next__(self):
value = self.current
self.current += 1
return value
# Infinite iterator (use break to stop)
counter = InfiniteCounter(1)
for num in counter:
print(num)
if num >= 5:
break
counter = InfiniteCounter(1)
17# Infinite iterator (use break to stop)18counter = InfiniteCounter(1)19for num in counter:self.current ← 1
4class InfiniteCounter:5 def __init__(self⟨InfiniteCounter A⟩, start1=0):6 self.current→ 1 = start1counter ← ⟨InfiniteCounter A⟩
17# Infinite iterator (use break to stop)18counter→ ⟨InfiniteCounter A⟩ = InfiniteCounter(1)19for num in counter:def __iter__(self):
8def __iter__(self⟨InfiniteCounter A⟩):9 return selfvalue ← 1, self.current ← 2
pass 1 of 511def __next__(self⟨InfiniteCounter A⟩):12 value→ 1 = self.current113 self.current→ 2 += 114 return value1All 5 passes — pass 1 is the card above pass numvalueself.current1 — 1 1 → 2 2 — 2 2 → 3 3 — 3 3 → 4 4 — 4 4 → 5 5 5 5 5 → 6 for num in counter:
pass 1 of 518counter = InfiniteCounter(1)19for num1 in counter⟨InfiniteCounter A⟩:20 print(num1)21 if num >= 5:output1All 5 passes — pass 1 is the card above pass num1 1 2 2 3 3 4 4 5 5 if num >= 5:
20print(num)21if num5 >= 5:22 break
Why It Matters
Understanding the iterator protocol helps you:
- Create custom iterables
- Understand how
forloops work - Debug iteration issues
- Implement lazy evaluation
Exercise: practical.py
Create a circular buffer iterator that loops through elements