Back to Blog
Python

Python StopIteration: How Iterators Signal Completion

python stopiteration: Learn how Python's StopIteration exception signals iterator completion, how to handle it with next() defaults, and why PEP 479 changed generator...

StopIterationiteratorsgeneratorsPEP 479next()exception handling
An illustration of a Python iterator reaching its final element and raising StopIteration to signal the end of iteration.

Understanding python stopiteration begins with the iterator protocol. When you call next() on an iterator that has no more items to produce, Python raises StopIteration. This exception is not an error in the usual sense; it is the standard signal that an iterator has been exhausted. Every iterator in Python relies on this mechanism to tell the caller that iteration is complete.

numbers = iter([1, 2, 3]) print(next(numbers)) # 1 print(next(numbers)) # 2 print(next(numbers)) # 3 print(next(numbers)) # raises StopIteration

The iter() call returns an iterator over the list. Each next() call advances the iterator and returns the next element. After the last element, next() raises StopIteration to indicate that there is nothing left to retrieve.

The Iterator Protocol and the Role of StopIteration

The iterator protocol is the contract that makes for loops, list comprehensions, and many other language features work. An object is iterable if it defines __iter__(), and an iterator defines both __iter__() and __next__(). The __next__() method is responsible for returning the next value or raising StopIteration when no values remain.

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

When a for loop iterates over an instance of Countdown, the interpreter repeatedly calls __next__() and catches StopIteration internally to terminate the loop. You never see the exception in normal iteration because the loop handles it for you.

Handling StopIteration with next() and Defaults

The most common way developers encounter StopIteration is through direct calls to next(). When you control the iterator and expect it to be exhausted, you can supply a default value so the exception is never raised:

values = iter([10, 20]) print(next(values, -1)) # 10 print(next(values, -1)) # 20 print(next(values, -1)) # -1

The two-argument form of next() returns the default instead of raising StopIteration. This is useful when you want to treat an exhausted iterator as a normal condition rather than an exceptional one. If you need to distinguish between "iterator exhausted" and "iterator produced a value equal to the default", you still need to catch the exception explicitly.

Why PEP 479 Changed StopIteration Inside Generators

Before Python 3.7, a StopIteration raised inside a generator body would propagate out of the generator and be seen by the caller. This caused subtle bugs when a generator was used in a context that expected iteration to continue. PEP 479 changed this behavior: a StopIteration raised inside a generator body is now converted to a RuntimeError.

def broken_generator(): yield 1 raise StopIteration for value in broken_generator(): print(value)

In Python 3.7 and later, this raises RuntimeError: generator raised StopIteration instead of silently ending the iteration. The change exists because a generator that raises StopIteration is almost always a bug: the generator is trying to signal completion from inside its own body, which is not the same as being exhausted by the caller.

Common Pitfalls with StopIteration

One frequent mistake is catching StopIteration too broadly. If you wrap a next() call in a try/except StopIteration block and the iterator's __next__() method itself has a bug that raises StopIteration for the wrong reason, you will silently swallow the problem.

Another pitfall is relying on StopIteration for control flow inside a generator. The PEP 479 change means that code which worked in Python 2 or early Python 3 versions may now raise RuntimeError. If you need to end a generator early, use return instead of raising StopIteration.

Using yield from to Delegate Iteration

The yield from expression delegates iteration to another iterable and handles StopIteration correctly. It is the clean way to compose generators without manually managing the inner iterator.

def outer(): yield from [1, 2, 3] yield 4 print(list(outer())) # [1, 2, 3, 4]

yield from forwards values from the inner iterable to the caller and propagates the inner StopIteration as the natural end of the delegated iteration. This avoids the need to write a manual loop with next() and exception handling.

Performance and Maintainability Considerations

Exception handling in Python has a runtime cost. Raising and catching StopIteration repeatedly in a tight loop is slower than using a loop construct that avoids exceptions altogether. For most code, the difference is negligible, but in performance-sensitive iteration paths, prefer next(iterator, default) over try/except when a default value is appropriate.

From a maintainability perspective, explicit next() calls with StopIteration handling make the iteration logic visible. That is useful when you need fine-grained control, but it also adds code that a for loop would handle automatically. Use direct next() calls when you genuinely need to control the iteration step by step; otherwise, prefer the for loop or yield from for clarity.

python stopiteration: Practical Usage and Code Examples | RYUSLOG DEV