Python iterators and the iterator protocol
__iter__, __next__, StopIteration, custom iterator class, iter() and next() built-ins, lazy evaluation, infinite iterators
The Iterator Protocol
Python's for loop works by calling iter() on an object to get an iterator, then repeatedly calling next() until StopIteration is raised.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
val = self.current
self.current -= 1
return val
for n in Countdown(3):
print(n) # 3 2 1 0
Using iter() and next() Directly
nums = [10, 20, 30]
it = iter(nums)
print(next(it)) # 10
print(next(it)) # 20
# next with default (no StopIteration)
print(next(it, -1)) # 30
print(next(it, -1)) # -1 โ exhausted
Infinite Iterators
class Counter:
def __init__(self, start=0):
self.n = start
def __iter__(self): return self
def __next__(self):
val = self.n
self.n += 1
return val
# Always pair with a break or islice
import itertools
for n in itertools.islice(Counter(), 5):
print(n) # 0 1 2 3 4
The iterator protocol is the foundation of Python's entire loop ecosystem. Every object that works with a for loop implements it โ lists, tuples, dicts, sets, files, ranges, and generators all follow the same protocol. This consistency is intentional design: new objects integrate with the whole language just by implementing two methods. The itertools module provides a rich set of iterator combinators โ chain, islice, product, groupby, cycle โ that compose with any iterator and are all lazy, making them memory-efficient for large data pipelines.
