Python Iterators: How for Loops Really Work Under the Hood
Imagine a vending machine. You press a button, it gives you one item. Press again, another item. You never see the entire inventory at once — you just get the next thing when you ask for it.
That's exactly how iterators work in Python. Every time you write a for loop, Python is secretly using an iterator behind the scenes. Understanding this mechanism unlocks the ability to build your own custom sequences, handle infinite data, and write memory-efficient code.
What Is the Difference Between an Iterable and an Iterator?
These two terms sound similar but they're different things. An iterable is anything you can loop over — lists, strings, dictionaries. An iterator is the object that actually does the work of fetching the next value.
Think of a book (iterable) versus a bookmark (iterator). The book contains all the pages, but the bookmark tracks where you are so you can get the next page.
The iter() function creates an iterator from an iterable. The next() function fetches the next value. When there are no more values, Python raises a StopIteration exception.
How Does the Iterator Protocol Work?
The iterator protocol is a contract. Any object that implements two special methods — __iter__() and __next__() — is an iterator. That's it. No inheritance required, no registration, just those two methods.
Notice that once an iterator is exhausted, it stays exhausted. You can't rewind it. If you want to loop again, you need a fresh iterator from the original iterable.
Predict what the following code will print. Think carefully about when StopIteration is caught.
nums = [10, 20, 30]
it = iter(nums)
print(next(it))
print(next(it))
print(next(it, 'done'))
print(next(it, 'done'))Write four print() statements that produce the exact same output.
How Do You Build a Custom Iterator Class?
Now for the fun part. You can make any object iterable by adding __iter__() and __next__() methods to its class. This is like building your own vending machine — you decide what comes out and in what order.
__iter__() returns the iterator object (usually self). __next__() returns the next value or raises StopIteration to signal the end. That's the whole contract.
Create a class called EvenRange that iterates over even numbers from start up to (but not including) stop. It should work like this:
for n in EvenRange(2, 10):
print(n)
# Output: 2 4 6 8Implement __iter__ and __next__ methods. If start is odd, begin from the next even number.
Can an Iterator Go On Forever?
Yes! Since the iterator decides when to raise StopIteration, it can simply... never raise it. This creates an infinite sequence. You just need to be careful to break out of the loop yourself.
Why Is Lazy Evaluation So Powerful?
Iterators are lazy — they produce values one at a time, only when asked. This is the opposite of a list, which stores everything in memory at once.
# All 1 million numbers in memory at once
nums = [x * 2 for x in range(1_000_000)]
print(nums[0])# Only one number in memory at a time
nums = (x * 2 for x in range(1_000_000))
print(next(nums))Lazy evaluation matters when you're dealing with large datasets, streaming data, or infinite sequences. You wouldn't load a 10GB file into a list — but you could iterate over it line by line using an iterator.
This Squares iterator should yield 1, 4, 9, 16, 25 (squares of 1 through 5). But it has two bugs. Find and fix them.
class Squares:
def __init__(self, n):
self.n = n
self.current = 0
def __iter__(self):
return Squares(self.n)
def __next__(self):
self.current += 1
if self.current > self.n:
raise StopIteration
return self.current * 2What Can You Do with itertools?
Python's itertools module is a toolbox of powerful iterator utilities. Instead of building everything from scratch, you can combine these building blocks.
Use itertools.cycle to assign 6 players to teams in round-robin order. Given teams = ['Alpha', 'Beta', 'Gamma'] and players = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank'], print each assignment as Player -> Team.
Create a class FibIterator that yields Fibonacci numbers up to a maximum value limit. The Fibonacci sequence starts with 0, 1, and each subsequent number is the sum of the previous two.
for n in FibIterator(20):
print(n)
# Should print: 0 1 1 2 3 5 8 13Use itertools.chain and a loop to combine three lists and print only the values greater than 5.
Lists: a = [1, 6, 3], b = [8, 2, 7], c = [4, 9, 5]
Print each qualifying number on its own line, in the order they appear.
What Should You Remember About Iterators?
Iterators are the engine behind every for loop in Python. The protocol is simple — implement __iter__() and __next__() — but the possibilities are vast. Custom iterators let you control exactly how data flows through your programs.
Key takeaways:
__iter__() and returns an iterator__iter__() and __next__()StopIteration signals the end of the sequenceitertools provides powerful, ready-made iterator utilities