Add the stop_exception parameter to iter() and aiter()

Add the stop_exception parameter to iter() and aiter()

The two-argument iter() only covers callables which report exhaustion by returning a special value. Many callables raise an exception instead — list.pop() and deque.popleft() raise IndexError, dict.popitem() raises KeyError, queue.get_nowait() raises Empty, asyncio.Queue.get() raises QueueShutDown after shutdown — and for them the sentinel form cannot be used at all. Every such loop is written by hand today.

This has been asked for several times: by Ram Rachum on python-ideas in February 2014, where Terry Reedy refined it from an exception in place of the sentinel into a separate third parameter; then as bpo-20663; and for aiter() on this forum in June. What follows is Reedy’s design, now with an implementation:

iter(iterable, /)
iter(callable, /, stop_value, *, stop_exception=StopIteration)
iter(callable, /, *, stop_exception)
for key, value in iter(d.popitem, stop_exception=KeyError):
    process(key, value)

for item in iter(queue.get_nowait, stop_exception=Empty):
    process(item)

stop_exception is an exception class or a tuple of exception classes, like the argument of an except clause. Either parameter can be given alone, or both together — whichever happens first ends the iteration. StopIteration keeps ending it in any case.

The same is proposed for aiter(), which has no callable form at all today. The callable is called and its result is awaited for every __anext__(), and StopAsyncIteration plays the role of StopIteration:

async for item in aiter(queue.get, stop_exception=QueueShutDown):
    process(item)

Two things have changed since then. queue.Queue.shutdown() and asyncio.Queue.shutdown() were added, so ending a consumer loop on an exception is now the standard way to drain a queue. And gh-119154, which asked for asyncio.Queue.__aiter__, was declined in 2025 because no single __aiter__ fits the many ways a queue is consumed, and because a for-loop front-end hides where the blocking happens and which exception is caught. The conclusion there was that users should write a small wrapper instead — this is that wrapper, spelled once in the language rather than once per class, with the blocking call and the terminating exception both visible at the call site.

Implementation: GH-156298.

Is a PEP needed for this feature?

15 Likes

IMO this is a reasonable addition, and shouldn’t need a PEP.

1 Like

I don’t know if a PEP is necessary or not, but this seems like an obvious addition to me as well, and I don’t have anything to add in terms of reasons to bikeshed or otherwise suggest it is less obvious.

Should that be the case? I’d expect that giving a value to the stop_exception argument would replace the value, not add to it.

2 Likes

StopIteration raised in __next__ is interpreted as the end of iteration in any case.

for x in iter(func, stop_exception=OtherError):
    ...

will end the loop in any case.

The only possible subtle difference is whether to mark the iterator as finished or allow iteration after the leaked StopIteration?

Or maybe replace the leaked StopIteration with RuntimeError as in generator functions (PEP 479)? This will prevent accidental stops.

Leaked StopIteration and/or AsyncStopIteration are now changed into RuntimeError. You always can add them to stop_exception to make them stopping iteration.

1 Like

Very reasonable and should perhaps be added as a note to the documentation of the feature?