How to Build a Python Custom Iterator
Learn how to implement a python custom iterator using __iter__ and __next__, when to return self, and when a generator is the simpler choice.
Python's for loop does not know how to traverse arbitrary objects. It relies on two methods: __iter__ and __next__. When you write for item in obj, Python calls iter(obj), which invokes obj.__iter__(). The object returned must have a __next__ method. Each iteration calls next(iterator), which invokes __next__, until the iterator raises StopIteration. At that point the loop terminates normally.
This protocol is the foundation for a python custom iterator. Any object that implements these two methods can be used in a for loop, in a list comprehension, or passed to functions like sum() and zip().
The Iterator Protocol
The protocol has two distinct roles. An iterable is an object that can produce an iterator, which means it implements __iter__. An iterator is an object that produces values one at a time, which means it implements __next__. A single object can fill both roles, but that design choice has consequences for reusability.
When Python encounters for item in obj, it calls iter(obj). If obj.__iter__ returns a new iterator each time, every loop starts from the beginning. If obj.__iter__ returns self, the second loop continues where the first one stopped, or raises StopIteration immediately if the sequence was already exhausted.
A Minimal Custom Iterator
Consider a countdown sequence. A list would work for a small fixed range, but suppose the sequence is expensive to compute or potentially infinite. A custom iterator computes each value only when requested.
class Countdown: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self): if self.current < 0: raise StopIteration value = self.current self.current -= 1 return value
Using it:
for number in Countdown(3): print(number)
The output is 3, 2, 1, 0. The __iter__ method returns self because Countdown is its own iterator. The __next__ method checks whether iteration should end, then returns the current value and updates the internal state. The state lives in the instance, so each Countdown object tracks its own position independently.
Returning Self vs. a Separate Iterator Object
The Countdown example returns self from __iter__. That works when the object is meant to be consumed once. But consider a class that represents a collection:
class Deck: def __init__(self, cards): self.cards = cards def __iter__(self): return iter(self.cards)
Here __iter__ returns a fresh iterator over the underlying list. You can iterate the same Deck multiple times, and each iteration starts from the beginning. If __iter__ returned self and __next__ advanced a position counter, the second for loop would start where the first one stopped.
The rule is simple: if the object is a one-shot sequence, returning self is fine. If the object represents a reusable collection, return a new iterator each time. The distinction matters for any python custom iterator that wraps an existing sequence.
Generators: The Simpler Alternative
Most custom iterators do not need a class. A generator function produces the same behavior with less boilerplate:
def countdown(start): while start >= 0: yield start start -= 1
The generator version handles __iter__ and __next__ automatically. Every call to countdown(3) returns a fresh generator object, so the sequence is reusable. State is kept in local variables rather than instance attributes.
Generators are the right default for most iteration logic. A class-based iterator is worth writing when you need additional methods, when the iteration logic must be shared with other state, or when the object must also be iterable in multiple ways.
Memory and Performance Behavior
A custom iterator produces values lazily. It never builds the full sequence in memory. This is the main performance advantage over materializing a list, especially for large or infinite sequences. The cost is a method call per item, which is slightly slower than iterating a list directly, but that overhead is usually irrelevant compared to the work done inside the loop body.
Infinite sequences are only safe with lazy iteration. A custom iterator that never raises StopIteration will loop forever, so any consumer must break explicitly. This is a deliberate design choice for streams and sensor data, but it also means the iterator cannot be passed to list() without hanging.
Common Mistakes and Edge Cases
Forgetting to raise StopIteration is the most common error. If __next__ returns a sentinel value instead, the for loop treats that value as a real item. The sequence silently includes an extra element.
Mutating the underlying data during iteration can also cause surprising behavior. If __next__ reads from a list that another part of the code modifies, the iterator may skip items or repeat them. This is not specific to custom iterators; it applies to built-in iterators too, but a custom iterator makes the dependency less obvious.
Another edge case is the iter() call on an object whose __iter__ is missing but whose __getitem__ is defined. Python falls back to calling __getitem__ with increasing integer indices until an IndexError is raised. Relying on that fallback is fragile and rarely intended; implement __iter__ explicitly.
When a Custom Iterator Is the Right Choice
Use a generator unless you have a concrete reason not to. A class-based python custom iterator earns its extra code when the iteration logic is stateful in a way that generators express poorly, when you need to expose the iterator as part of a larger class API, or when you must support multiple independent iteration passes over the same object.
The decision also depends on whether the object is a one-shot iterator or a reusable iterable. If you need both, implement the iterable as a container that returns a fresh iterator from __iter__, and implement the iterator as a separate class with __next__. That separation keeps the two roles clear and prevents the state of one iteration from leaking into the next.