Python's for loop, comprehensions, * unpacking, and built-ins like sum and min all work on anything that follows a simple contract: the iterator protocol. The contract is two dunder methods, __iter__ and __next__, and one exception, StopIteration. This chapter shows exactly what each piece does, how for desugars into calls to those methods, and the two-argument form of iter() that turns a callable into an iterator.
Two roles matter here: an iterable is something you can loop over, and an iterator is the thing that actually walks through the items one at a time. The iterator protocol is what makes those roles concrete.
An object plays the protocol by implementing dunder methods:
| Role | Required Method | What It Returns |
|---|---|---|
| Iterable | __iter__(self) | A fresh iterator over the iterable's items. |
| Iterator | __next__(self) | The next item, or raises StopIteration when done. |
| Iterator | __iter__(self) | self. Iterators are also iterables. |
That's the whole protocol. Three slots, one exception. Everything in Python that loops, comprehends, unpacks, or otherwise consumes a sequence ultimately calls these methods.
A short demo with a built-in iterable makes the slots visible:
The list itself doesn't have __next__. Calling cart.__iter__() produced a separate list_iterator object, and that's the object that knows how to walk the list. Each __next__() call returned the next item until there were none left. The fourth call raised StopIteration to signal the end.
The built-in functions iter() and next() are friendlier names for the dunders. You almost never call __iter__ or __next__ directly in real code:
iter(obj) calls obj.__iter__(). next(it) calls it.__next__(). Same operation, cleaner syntax.
The cleanest way to describe the difference between an iterable and an iterator is by which methods each one has.
| Object | Has __iter__? | Has __next__? | Calling iter() returns |
|---|---|---|---|
| Iterable (list, tuple, dict, str, set) | yes | no | A new iterator over its items |
Iterator (returned by iter()) | yes (returns self) | yes | The same object |
A list is iterable. It has __iter__ but not __next__. Try to call next() on a list directly and Python tells you off:
The list doesn't track a position. It can't tell you what the "next" item is because it has no notion of "current." That state has to live somewhere, and that somewhere is the iterator object that iter(cart) creates.
Calling iter() on the same iterable twice gives you two independent iterators, each with its own position:
it1 and it2 walk the same list but keep separate cursors. This is exactly what makes nested loops over the same list work without surprises.
The diagram shows the relationship. One iterable. Many iterators, each with private state. When you call iter() on the iterable, you ask it to manufacture a fresh walker.
The protocol requires iterators to implement __iter__ and return self. At first this sounds odd, since an iterator is the thing doing the walking, not the thing being walked. Why does it need to advertise itself as iterable too?
The reason is so iterators can be used directly in for loops, comprehensions, and any function that expects an iterable. The for statement always starts by calling iter() on its target. If iterators didn't return themselves from __iter__, you'd get an error every time you tried to loop over a raw iterator.
Here's the rule in action. Take an iterator, pass it back through iter(), and you get the same object:
iter(it) doesn't make a new iterator. It returns the same one. That's what "iterators are iterables" means at the implementation level.
The practical effect is that this works:
The for loop internally calls iter(it), which gives back it itself, and then calls next() on it until StopIteration.
The catch: an iterator is single-use. Once exhausted, it stays exhausted. Looping over the same iterator twice does nothing the second time:
The second for immediately gets StopIteration on the first next() call and exits without printing anything. The iterator's internal cursor is sitting past the end, and nothing in the protocol resets it. If you want to walk the data again, ask the iterable for a new iterator.
Cost: Manufacturing a new iterator from a built-in iterable like list is O(1). It allocates one small object and stores a reference to the list and an index. The work happens later as you call next().
for x in obj: DesugarsThe for loop is the most common consumer of the iterator protocol, and it's worth seeing exactly what it does behind the scenes. This loop:
is roughly equivalent to this while loop:
Three steps. First, call iter() on the target once, at the start. Second, repeatedly call next() to pull items, binding each one to the loop variable. Third, catch StopIteration and break out of the loop. The for statement isn't magic. It's a tight wrapper around these three operations.
This desugaring explains several things that otherwise feel arbitrary:
__iter__ all pass step one.StopIteration immediately.__iter__ returns a key iterator, so the desugared next() calls yield keys.__next__ to read the next line and raise StopIteration at end of file.Other constructs use the same desugaring. List comprehensions, generator expressions, tuple unpacking, sum, min, max, any, all, sorted, and the in operator on most types all start with iter() and consume with next().
The flow shows one full pass of a for loop. iter() runs once. next() runs in a tight loop until StopIteration is raised, at which point the exception is swallowed and control moves past the loop.
StopIteration Is How an Iterator Says "I'm Done"The protocol has no "is there a next item?" method. There's no has_next, no boolean check. Instead, an iterator says "I'm done" by raising StopIteration from __next__. The caller is expected to catch that exception or use a construct (like for) that catches it automatically.
This makes __next__ strictly two-state: either return a value, or raise StopIteration. Nothing else is a valid "end" signal.
Once an iterator raises StopIteration, it stays exhausted. Calling next() again raises StopIteration again. The iterator never magically resets.
The first list(nums) drained the iterator. The second got nothing because the iterator was already done.
next() also accepts an optional default argument. If you pass one, StopIteration is converted into a return of that default value instead of being raised:
This is occasionally cleaner than a try/except when you just want to peek at the next item with a fallback.
The protocol becomes concrete the moment you implement it on your own class. Here's the smallest example that exercises both methods, just to make the contract tangible.
Three things to notice. First, __iter__ returns self because this class is its own iterator. Second, __next__ checks the end condition first and raises StopIteration before doing anything else. Third, the state (self.current) lives on the instance, which is why the iterator can pause and resume between next() calls.
This class works in any context that consumes an iterable:
list(), sum(), and the generator expression all call iter() on the CartCounter instance (which returns the instance itself), then next() until StopIteration. The class needed no extra code to be compatible with these built-ins because it spoke the protocol.
Most non-trivial custom iterators are easier to write as full classes or as generator functions. The hand-rolled class above is the lowest-level form, but it's rarely the cleanest.
Cost: Each next() call is a method call plus whatever work __next__ does. Generator functions often produce identical iteration with less code and slightly less per-call overhead because their state machine is implemented in C.
iter(callable, sentinel) Formiter() has a less-known second form that takes two arguments: a zero-argument callable and a sentinel value. It builds an iterator that calls the callable each time next() is called and stops as soon as the callable returns the sentinel.
The shape:
Each call to next(it) calls callable_with_no_args(). If the result equals sentinel, StopIteration is raised. Otherwise the result is the next item.
A small e-commerce example: ask the customer to enter products to add to their cart, one per line, until they type done:
Sample run:
Without the two-argument form, you'd write a while loop with an explicit break:
Both versions do the same thing. The iter(callable, sentinel) form turns the loop into an iterator that any consumer (a for loop, a comprehension, list(), sum()) can use.
A useful pattern this enables: reading a file in fixed-size chunks until the end of the file. file.read(n) returns an empty bytes/string when the file is exhausted, which makes b"" (or "") a natural sentinel:
The lambda is a zero-argument callable that returns the next 8-byte chunk. The sentinel b"" marks end of file. The result is a clean for loop over fixed-size blocks, without a hand-written while True and break.
The callable must take no arguments. If yours does, wrap it in a lambda or functools.partial:
The two-argument form is one of those features that's easy to overlook but pays off the moment you have a "call this thing repeatedly until it signals done" pattern.
Reading the diagram in plain language:
__iter__. That method returns an iterator.__iter__, but it returns itself. This is what lets iterators stand in for iterables in any consumer.__next__. Each call returns the next item, or raises StopIteration if there are none left.StopIteration is the only way to signal end-of-iteration.Every loop, comprehension, unpacking, and iteration-consuming built-in in Python is built on these four facts.
A few patterns trip up beginners more than others. Each one is easy to spot once you know what to look for.
StopIterationThe single most common iterator bug: __next__ returns a sentinel value (like None) instead of raising StopIteration. The for loop never sees the end-of-iteration signal and either yields the sentinel as a normal value or loops forever.
What's wrong with this code?
The author meant Countdown(3) to yield 3, 2, 1 and stop. But because __next__ returns None instead of raising StopIteration, the loop keeps going and None shows up as a regular value. The if n is None: break is a workaround, not a fix.
Fix:
StopIteration is the only legitimate "I'm done" signal. Returning None is just returning None.
self From __next__A different mix-up: putting return self inside __next__ instead of __iter__. The class then "iterates" by yielding the iterator object itself on every call, which a for loop will happily do forever.
What's wrong with this code?
The loop would run forever without the break. __next__ is supposed to return the next item, not the iterator. __iter__ is the method that returns self.
Fix:
Remembering which method returns self is straightforward once you internalize the contract. __iter__ returns "the iterator" (which, for an iterator object, is self). __next__ returns "the next item" (which is never the iterator itself).
iter() Thinking It Resets the Iteratoriter() on an iterable makes a new iterator. iter() on an existing iterator returns the same iterator. Beginners sometimes call iter() on an exhausted iterator hoping to reset it.
What's wrong with this code?
The second loop prints nothing. iter(it) returned the same exhausted iterator. To get fresh iteration, call iter() on the original iterable, not on the iterator you already drained.
Fix:
The lesson: an iterator's __iter__ returns itself. That's a feature for for-loop compatibility, but it does mean you can't "reset" an iterator by passing it through iter(). State has to come from somewhere, and an exhausted iterator has no fresh state to give you.
A more subtle bug: a function accepts an iterable, then tries to walk it more than once. If the caller passed a list, both walks work. If the caller passed an iterator (or a generator), the second walk gets an empty iterator.
With the list, both sum calls walked the list independently. With the iterator, the first sum drained it and the second saw nothing. The classic fix is to convert to a concrete container at the boundary: items = list(items) at the top of the function if you intend to walk it more than once.
10 quizzes