Back in the Loops chapter, we introduced the word iterable - anything a for loop knows how to walk through, one item at a time. We’ve used plenty of iterables since then: lists, strings, ranges, dictionaries, sets, tuples. Now that you know classes and dunder methods, we can finally open the hood and see exactly what makes something iterable in the first place.
These two words look almost identical, and that trips people up, so let’s be precise:
-
An iterable is anything you can loop over - a list, a string, a range.
-
An iterator is the actual object doing the walking - it remembers exactly where it currently is, and knows how to fetch the next item.
Here’s the relationship: an iterable doesn’t walk itself. When a for loop wants to walk through one, it first asks the iterable for an iterator, and that iterator is what actually keeps track of position and hands back values, one at a time.
You can do this yourself, without a for loop at all, using two built-in functions: iter() and next().
donut_flavors = ["chocolate", "glazed", "jelly"]
flavor_iterator = iter(donut_flavors)
print(next(flavor_iterator)) # -> chocolate
print(next(flavor_iterator)) # -> glazed
print(next(flavor_iterator)) # -> jellyiter() takes an iterable and hands you back its iterator. Each call to next() moves that iterator forward by one and gives you the next value. What happens if you call next() one time too many, once there’s nothing left?
print(next(flavor_iterator))
# StopIterationPython raises a StopIteration error. That’s not really a "problem" - it’s the iterator’s official way of saying "I’m out of items, we’re done here."
Here’s the secret: a for loop is just doing exactly what we did above, automatically, and quietly catching StopIteration for you so your program doesn’t crash.
donut_flavors = ["chocolate", "glazed", "jelly"]
flavor_iterator = iter(donut_flavors)
while True:
try:
flavor = next(flavor_iterator)
except:
break
print(flavor)That clunky while True / try / except mess is precisely what for donut in donut_flavors: is doing behind the scenes, every single time you write it. This is why for works identically on a list, a string, a dictionary, or anything else iterable - they all agree to answer iter() and next() the same way, so for never needs to know or care what kind of thing it’s actually looping over.
Since __iter__ and __next__ are just two more dunder methods, you can add them to any class you write, and suddenly a for loop will work on your own objects too.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for count in Countdown(3):
print(count)
# output is
# 3
# 2
# 1
# 0Let’s break down the two dunders doing the work here:
-
__iter__needs to return an iterator - here, we just returnself, since ourCountdownobject is perfectly capable of tracking its own position and handing back its own next value. -
__next__does the real work: it computes the next value, and once there’s nothing left to give, it raisesStopIterationitself, exactly like the built-in iterators did.
Because Countdown defines both, Python’s for loop is happy to walk right through it, exactly like it would a list or a range.
|
Tip
|
|
All of this - iter(), next(), StopIteration, __iter__, __next__ - is the real machinery under every for loop you’ve ever written. It’s a little bit of ceremony to write by hand, though. In the next chapter, we’ll meet generators - Python’s much shorter way to build your own iterator, without writing a whole class at all.