The iterator protocol says that any object with __iter__ and __next__ can be looped over by a for statement. This lesson is about building those objects yourself, from scratch, as classes. You'll see how to wire up the two methods, when to put them on the same class, when to split them into two classes, and which design bugs trip up beginners every single time.
Most data containers you reach for in everyday Python already iterate. Lists, tuples, dicts, sets, files, even strings know how to feed values to a for loop. The cases where you'd write your own iterator look like this:
for loop instead of memorizing your traversal API.The mechanics are simple once you've seen them a few times. The design choice between "one class" and "two classes" is the part worth being careful about, and it's the part most lessons gloss over.
Before going further, a quick refresher on the vocabulary:
iter(). It has an __iter__ method that returns a fresh iterator.iter() returns. It has a __next__ method that yields the next value, or raises StopIteration when there's nothing left. By contract, an iterator's own __iter__ returns self.That distinction (between iterables and iterators) is the whole reason this lesson has two patterns instead of one.
Here's the smallest useful example: a class that walks a list of product names and yields them one at a time.
Three things are happening here, and they matter.
__init__ stores the data and sets up a cursor. The cursor is the integer self.index, which tracks where we are in the walk.
__iter__ returns self. The for loop calls iter(walker) once at the start to get an iterator. Since ProductWalker is already an iterator (it has __next__), it can hand back itself. This is the "self-iterator" pattern.
__next__ does the actual work. It either returns the next value and advances the cursor, or raises StopIteration when there are no values left. The for loop catches that exception silently and stops looping.
Trace through it once by hand:
for name in walker calls iter(walker), which calls walker.__iter__(), which returns walker itself.next(walker). index is 0, so it returns "mug" and sets index to 1.next(walker) returns "notebook", index becomes 2."pen", index becomes 3.index >= 3, so __next__ raises StopIteration. The loop catches it and exits.The ProductWalker above works, but it has a flaw that beginners hit the first time they try to loop twice:
The second loop produces nothing. The walker is exhausted. Its index is sitting at 3, and __next__ raises StopIteration immediately.
This is the natural consequence of the self-iterator pattern. The walker is the iterator, so its state and the iteration state are the same thing. Once you've moved the cursor past the end, the only way to start over is to build a new walker.
For some use cases that's fine. Generators work this way too. For others, it's a real problem, especially when you want a container that callers can iterate over multiple times like a list does. That's where the two-class pattern comes in.
There are two ways to wire up __iter__ and __next__, and the choice shapes how your object behaves in real use. Here they are at a glance:
In the self-iterator pattern, one class plays both roles. __iter__ returns self, so every call to iter(obj) hands back the same object with its same cursor state. Pros: short, easy to write. Cons: one-shot. Once consumed, the object is done.
In the two-class pattern, a container class holds the data and defines __iter__. Each time __iter__ is called, it builds a fresh iterator object that holds its own cursor. The container itself has no cursor state. Pros: works with multiple for loops over the same container, just like a list. Cons: more code, and you have to remember which class owns which method.
Here's the same data, written both ways. First the self-iterator (one-shot):
The second list(walker) is empty because the cursor is already at the end. Now the two-class version:
Both list(catalog) calls produce the same result, because each one builds a brand-new ProductCatalogIterator with its own index = 0. The container has no cursor; the iterator does.
Which one to use? Rule of thumb:
| Pattern | Use when |
|---|---|
| Self-iterator (one class) | The object is conceptually a stream: pages of API results, lines of a log, a one-time scan that wraps state you don't want to share. Re-iteration doesn't make sense. |
| Container + iterator (two classes) | The object is conceptually a collection: a catalog, an order history, a category tree. Callers reasonably expect to loop over it more than once, the way they would loop over a list. |
Cost: The two-class pattern allocates a new iterator object per for loop. That's a cheap allocation, and it's almost always the right trade for the multi-pass behavior you get. Don't optimize it away.
A real case where the self-iterator pattern fits well: pagination. You want to walk through a product catalog one page at a time, and re-iterating doesn't really make sense, because the act of iterating is the act of pulling pages.
The paginator yields one page per __next__ call. The last page is shorter than page_size, which is exactly what you want for the trailing items. When the index runs past the list, StopIteration ends the loop.
Notice we used enumerate(paginator, start=1) to number the pages. enumerate works because the paginator is iterable, and that's the whole point of the protocol: any code that takes an iterable accepts your custom object for free.
A few design notes worth pausing on:
self.index) lives on the paginator itself. That's fine because we don't expect to iterate over the same paginator twice. If a caller wants a fresh walk, they construct a new paginator.self.products[self.index : self.index + self.page_size] is safe even at the end. Python's slice never raises IndexError; it just gives back whatever fits.StopIteration check uses >=, not ==. With >=, the loop ends correctly even if self.index overshoots len(self.products), which it does on the last advance.Cost: Each page slice allocates a new list. If page_size is large and pages are rarely consumed in full, you're copying more than you need. A generator can yield items lazily and avoid building the page list at all.
Now a case where the two-class pattern is the right choice. An Inventory object holds the products currently in stock, and callers want to loop over it multiple times: once to compute total value, once to print a report, once to filter low-stock items.
Wait, the print order looks odd. Let me adjust the example so the output is in the order items were added.
Three separate passes, three independent walks of the same data. That's the payoff for the two-class split: each for loop calls inventory.__iter__(), which constructs a new InventoryIterator with its own _index. The passes don't interfere with each other, because they're not sharing cursor state.
Try collapsing this into a single class to see why the two-class split matters here:
What's wrong with this code?
The first walk works. The second is empty, because _index is now sitting past the end of the list. The container is also the iterator, and one cursor is shared across every loop that ever touches this object. To callers, the inventory looks "broken" after the first iteration. The fix is the two-class pattern above: keep the cursor off the container, on a separate iterator that gets rebuilt every time.
Custom iterators are also useful when the natural iteration order differs from the storage order. Suppose OrderHistory stores customer orders in the order they were placed, but the UI wants to display them newest first.
The storage stays in chronological order, but the iterator walks it backward. Callers don't have to know that; they just write for order in history. The reverse-walk logic is hidden inside OrderHistoryIterator.__next__.
Two details worth noticing:
self._index < 0, not <= 0. We want index 0 (the oldest order) to be included, so the check has to allow 0 through.The previous examples walked a flat list. Custom iterators really earn their keep when the underlying structure isn't flat. A product category tree is a good example: categories contain subcategories, which contain more subcategories, and so on.
Here's the data model: a Category has a name and a list of children.
The tree looks like this:
We want a depth-first walk: Home, then Electronics, then Laptops, then Phones, then Home Goods, then Mugs, then Notebooks. The trick for doing this iteratively is to use an explicit stack. Push the root, then on every __next__, pop a node, return its name, and push its children in reverse order (so the first child comes off the stack next).
Trace the stack to see why this works. Start with [Home]. Pop Home, return "Home", push children in reverse (Home Goods, then Electronics), so the stack is now [Home Goods, Electronics]. Pop Electronics (top of the stack), return "Electronics", push its children in reverse (Phones, then Laptops), so the stack is [Home Goods, Phones, Laptops]. Pop Laptops, return "Laptops", no children to push. Pop Phones, return "Phones". Pop Home Goods, push its children in reverse, and so on until the stack is empty.
Two more things to note:
for builds a fresh CategoryTreeIterator with a fresh stack.Cost: The stack can grow as large as the deepest path in the tree, plus one extra entry per sibling along that path. For balanced trees that's O(log n); for skewed trees it can be O(n). Either way it's far cheaper than building a list of every node up front.
Custom iterators have a small set of failure modes that hit beginners over and over. Here are the four you'll see most.
__iter__This is the bug that hides for the longest because the first iteration works fine.
What's wrong with this code?
The first walk consumed the catalog. The second walk got nothing, because __iter__ returned the same object, which still has index = 3 from the previous loop. The container looks like a one-shot iterator to every piece of code that touches it.
Fix: Split into a container and a separate iterator class. __iter__ on the container builds a new iterator object each time, with a fresh cursor.
If your class is conceptually a container (something a caller would expect to iterate more than once), __iter__ must build a fresh iterator.
StopIterationThe protocol uses an exception to signal "done." If __next__ reaches a state where there's nothing left to return and the function just falls off the end, it returns None, and None is a perfectly valid item from the loop's perspective.
What's wrong with this code?
Run this in a loop and Python keeps calling __next__ forever. Each call returns None, the loop prints it, and there's nothing to stop it. The fix is the missing line:
Fix:
There's no clever trick here. The protocol just requires the exception, and Python won't infer the end of iteration from anything else.
If the iterator holds a reference to the container's internal list, mutating that list mid-walk produces results that are at best surprising and at worst wrong.
pen was skipped. The iterator's _index was 2 (pointing at pen) when we removed pen. The remove shifted lamp down to index 2, but the iterator then advanced to index 3, jumping over the position entirely.
This is the same trap that built-in containers warn about (the docs for list and dict both note that mutation during iteration is undefined). The safe pattern is to walk a copy or to collect changes and apply them after the loop:
Custom iterators inherit the same hazard. If your container exposes mutation, document that callers shouldn't mutate mid-iteration, or take a snapshot inside __iter__:
The snapshot adds a copy on each __iter__ call, which is O(n) in the size of the container. For small wishlists it's fine; for very large catalogs you'd think harder about whether the safety is worth the cost.
Closely related to Bug 1: even with a two-class pattern, if the iterator mutates state that the container also reads, multiple simultaneous iterations can collide.
The iterator class exists, but it stores the cursor on the container instead of on itself. Two simultaneous loops over the same Bad object share _cursor, and the second loop never starts from the top. The fix is to give each iterator its own cursor, like InventoryIterator and OrderHistoryIterator did.
Every example in this lesson would be shorter as a generator function. A generator is a function that uses yield instead of return, and Python builds the iterator class for you behind the scenes. Here's the ProductCatalogPaginator rewritten as a generator, for a sneak peek:
No class, no __iter__, no __next__, no manual StopIteration. Python wires it all up from the yield statements. Most of the iterators in this chapter would be cleaner as a generator function.
The reason this chapter still matters: generators have constraints that classes don't (you can't easily reset them, you can't pickle them, you can't add extra methods to the iterator object). When you need an iterator with more shape than yield gives you, knowing how to build one by hand is the difference between "Python won't let me" and "I'll just write the class."
9 quizzes