Iterators
Every time you write a for loop over a list, a file, or a dictionary, Python is quietly running a small, well-defined protocol behind the scenes called the iterator protocol. Understanding it is one of those things that makes a lot of "magic" Python behavior click into place — why some objects can be looped over and others can't, why you can only loop over a file once, and how tools like generators (covered next) actually work under the hood.
The iterator protocol
An object is an iterator if it implements two special methods:
class SomeIterator:
def __iter__(self):
# Must return the iterator object itself
return self
def __next__(self):
# Must return the next value, or raise StopIteration
# when there are no more values left
...__iter__ returns the iterator itself, and __next__ produces the next value in the sequence each time it's called. When there's nothing left to produce, __next__ raises the built-in StopIteration exception instead of returning a value. That's the entire contract — no more, no less.
Iterable vs. iterator
These two words look similar and are often used loosely, but they mean different things. Mixing them up is one of the most common sources of confusion for people learning how Python loops work.
Aspect | Iterable | Iterator |
|---|---|---|
Required methods | iter only | iter and next |
Can be passed to iter() | Yes — produces an iterator | Yes — typically returns itself |
Keeps track of position | No | Yes, internally |
Can be looped over more than once | Usually yes (list, tuple, str) | No — consumed after one pass |
Examples | list, tuple, dict, str, set | the object returned by iter([1, 2, 3]) |
In short: a list is iterable — you can ask it for an iterator as many times as you like — but the iterator you get back is a one-way, single-use object. Once it's exhausted, it stays exhausted.
iter() and next()
The built-in iter() function turns an iterable into an iterator, and next() pulls the next value out of an iterator. You can drive this manually instead of using a for loop:
numbers = [1, 2, 3] it = iter(numbers) print(next(it)) print(next(it)) print(next(it)) print(next(it)) # nothing left!
1 2 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
That traceback is exactly what it looks like: StopIteration is a perfectly normal exception that signals "there is nothing left to give you." It isn't a crash or a bug — it's the mechanism the entire iteration protocol is built on. You almost never see it directly, because a for loop catches it internally and quietly ends the loop instead of letting the exception propagate. It only becomes visible if you call next() yourself past the end, as above.
Building a custom iterator
Because the protocol is just two methods, you can build your own iterator classes for custom behavior. Here's a Countdown iterator that counts down from a starting number to zero:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
# An iterator's __iter__ returns itself
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return valueWith those three methods in place, Countdown can be used anywhere Python expects an iterable — most naturally, in a for loop:
for number in Countdown(5):
print(number)5 4 3 2 1 0
How for loops really work
This is the part that ties everything together: a for loop is not a special piece of syntax that magically understands lists, ranges, and custom classes. It's just a convenient wrapper around the exact manual pattern shown earlier. Roughly, this:
for number in Countdown(5):
print(number)is equivalent to this:
iterator = iter(Countdown(5))
while True:
try:
number = next(iterator)
except StopIteration:
break
print(number)The for loop calls iter() on whatever you hand it to obtain an iterator, then repeatedly calls next() on that iterator, printing or binding each value, until StopIteration is raised — at which point it stops silently. This is exactly why any object that correctly implements __iter__ and __next__ can be looped over with for, regardless of whether it's a built-in type or a class you wrote yourself.
iter() hands back a fresh iterator starting from the beginning.__iter__ and __next__ by hand, as shown above, works — but it's verbose for anything non-trivial. Generators, covered on the next page, let you write the exact same kind of lazy, stateful iteration using a plain function and the yield keyword, with Python building the iterator machinery for you automatically.