PythonIterators

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:

Python
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:

Python
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:

Python
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 value

With those three methods in place, Countdown can be used anywhere Python expects an iterable — most naturally, in a for loop:

Python
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:

Python
for number in Countdown(5):
    print(number)

is equivalent to this:

Python
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.

Note
Because an iterator tracks its own position and is consumed as you go, looping over the same iterator object twice gives you nothing the second time — it's already exhausted. Iterables like lists don't have this problem because each call to iter() hands back a fresh iterator starting from the beginning.
Tip
Writing a class with __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.