AlgoMaster Logo

Custom Iterators

Medium Priority22 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

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.

Why Write a Custom Iterator at All

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:

  • You're walking a custom structure (a category tree, a paginated catalog, an order history) and you want callers to use a plain for loop instead of memorizing your traversal API.
  • You want results to be lazy: produced one at a time, on demand, instead of building a giant list in memory.
  • You want to control how the iteration is ordered (reverse chronological, depth-first, oldest first) without exposing the underlying storage.

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:

  • An iterable is anything you can pass to iter(). It has an __iter__ method that returns a fresh iterator.
  • An iterator is what 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.

The Minimal Custom Iterator

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:

  1. for name in walker calls iter(walker), which calls walker.__iter__(), which returns walker itself.
  2. The loop calls next(walker). index is 0, so it returns "mug" and sets index to 1.
  3. Next iteration: next(walker) returns "notebook", index becomes 2.
  4. Next: returns "pen", index becomes 3.
  5. Next: index >= 3, so __next__ raises StopIteration. The loop catches it and exits.

The One-Shot Problem with Self-Iterators

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.

Two Patterns Side by Side

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:

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

Worked Example 1: ProductCatalog Paginator

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:

  • The state (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.
  • Slicing 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.
  • The StopIteration check uses >=, not ==. With >=, the loop ends correctly even if self.index overshoots len(self.products), which it does on the last advance.

Worked Example 2: Inventory Walker (Two-Class)

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.

Worked Example 3: OrderHistory in Reverse Chronological Order

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:

  • The iterator stores a reference to the same list the container holds. That's fine for read-only iteration. If the container's list grows or shrinks during iteration, things get messy, which is the next bug we'll look at.
  • The stopping condition is self._index < 0, not <= 0. We want index 0 (the oldest order) to be included, so the check has to allow 0 through.

Worked Example 4: CategoryTree Depth-First Iterator

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:

  • This walks iteratively, using a list as a stack. The depth-first order is identical to what you'd get from a recursive walk, but you don't pay function-call overhead per node, and you don't hit Python's recursion limit on deep trees.
  • The two-class split matters here for the same reason as before: a caller might iterate the tree multiple times (once for a count, once for a report). Each for builds a fresh CategoryTreeIterator with a fresh stack.

Common Bugs

Custom iterators have a small set of failure modes that hit beginners over and over. Here are the four you'll see most.

Bug 1: Container Returns Self From __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.

Bug 2: Forgetting to Raise StopIteration

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

Bug 3: Mutating the Container While Iterating

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.

Bug 4: Sharing State Between Iterators

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.

When to Reach for a Generator Instead

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

Quiz

Custom Iterators Quiz

9 quizzes