Skip to content

gh-64862: Add the stop_exception parameter in iter() and aiter() - #156298

Open
serhiy-storchaka wants to merge 5 commits into
python:mainfrom
serhiy-storchaka:calliter-stop-exception
Open

gh-64862: Add the stop_exception parameter in iter() and aiter()#156298
serhiy-storchaka wants to merge 5 commits into
python:mainfrom
serhiy-storchaka:calliter-stop-exception

Conversation

@serhiy-storchaka

Copy link
Copy Markdown
Member

iter() and aiter() now accept the keyword-only stop_exception parameter -- an exception class or a tuple of exception classes which ends the iteration:

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

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

Many callables report exhaustion by raising an exception instead of returning a special value, so the sentinel form cannot be used with them at all.

aiter() also gained the callable form, which it did not have before: the callable is called and its result is awaited for every __anext__() (the callable is only called when the result of __anext__() is awaited).

The second parameter of iter() is now named stop_value and can be passed by keyword. It can be omitted if stop_exception is given.

stop_exception=StopIteration (StopAsyncIteration for aiter()) and an empty tuple never change the behavior, so they are normalized to "no stop exception"; such an iterator is pickled exactly as before. For other cases callable_iterator now has __setstate__(), because the stop exception and the absence of the sentinel cannot be expressed as arguments of iter().

The created iterator stops when the callable raises the specified
exception.  The second parameter of iter() is now named stop_value and
can be passed as a keyword argument.

aiter() now accepts the same stop_value and stop_exception parameters,
calling an asynchronous callable and awaiting the result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@read-the-docs-community

read-the-docs-community Bot commented Aug 23, 2026

Copy link
Copy Markdown

serhiy-storchaka and others added 3 commits August 23, 2026 23:41
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Use StopIteration (StopAsyncIteration for aiter()) as the default instead
of normalizing it to NULL, so that the check is a single
PyErr_ExceptionMatches().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
If the callable raises StopIteration (StopAsyncIteration in aiter()) which
does not match stop_exception, the consumer would mistake it for the end of
the iteration, or, in the asynchronous case, for the result of the await.
Replace it with RuntimeError, as PEP 479 and PEP 525 do for generators.

StopIteration is therefore no longer special: it stops the iteration only
because it is the default stop_exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread Doc/library/functions.rst Outdated
aiter(callable, /, stop_value, *, stop_exception=StopAsyncIteration)
aiter(callable, /, *, stop_exception)

Return an :term:`asynchronous iterator` for an :term:`asynchronous iterable`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have wording similar to iter() -- “The first argument is interpreted very differently...”

Comment thread Doc/library/functions.rst Outdated
Comment on lines +1222 to +1224
from queue import Empty
for item in iter(queue.get_nowait, stop_exception=Empty):
process_item(item)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Avoid using the name queue for two different things:

Suggested change
from queue import Empty
for item in iter(queue.get_nowait, stop_exception=Empty):
process_item(item)
import queue
for item in iter(input_queue.get_nowait, stop_exception=queue.Empty):
process_item(item)

Comment thread Objects/iterobject.c Outdated
Comment on lines +191 to +192
/* Both are set to NULL when the iterator is exhausted */
PyObject *it_callable;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“Both” doesn't make sense with 3 items. Should all be NULLed on exhaustion?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was related only to it_callable and it_sentinel. Reworded.

it_stop_exc is not NULLed intentionally. In case of reentrant __next__ call (usually a concurrent use) we can get an exception, after the iterator was exhausted. Without it_stop_exc we cannot distinguish a StopIteration which stops iteration from StopIteration which should be converted to RuntimeError.

it_callable and it_sentinel should be NULLed because they can keep large objects, but it_stop_exc is normally just a type or a tuple of types.

Comment thread Objects/iterobject.c
Comment on lines +705 to +729
PyTypeObject _PyACallIter_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"async_callable_iterator", /* tp_name */
sizeof(acalliterobject), /* tp_basicsize */
0, /* tp_itemsize */
/* methods */
acalliter_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
&acalliter_as_async, /* tp_as_async */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
0, /* tp_doc */
acalliter_traverse, /* tp_traverse */
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: .tp_dealloc = acalliter_dealloc, etc. for new code.

Comment thread Objects/iterobject.c
}

static void
acalliter_exhaust(acalliterobject *it)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this in sync iter as well, for symmetry?

Comment thread Objects/iterobject.c Outdated
PyObject_HEAD
PyObject *aw_iterator; /* the iterator which created this object */
PyObject *aw_wrapped; /* the awaitable returned by the callable */
char aw_closed;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: we can use bool internally.

Comment thread Lib/test/test_iter.py

# Test iter() with the exception argument
def test_iter_exception(self):
self.check_iterator(iter(CallableIterClass(), stop_exception=IndexError),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Emergency stop comment above is now outdated.

Comment thread Lib/test/test_asyncgen.py
# A StopAsyncIteration leaking from the await is replaced with
# RuntimeError (see PEP 525)
async def spam():
raise StopAsyncIteration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test raising StopIteration here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would not test the aiter() code. A StopIteration is converted to RuntimeError by the coroutine machinery before the iterator sees it.

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review. Applied suggestions, answered questions.

Comment thread Lib/test/test_asyncgen.py
# A StopAsyncIteration leaking from the await is replaced with
# RuntimeError (see PEP 525)
async def spam():
raise StopAsyncIteration

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would not test the aiter() code. A StopIteration is converted to RuntimeError by the coroutine machinery before the iterator sees it.

Comment thread Objects/iterobject.c Outdated
Comment on lines +191 to +192
/* Both are set to NULL when the iterator is exhausted */
PyObject *it_callable;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was related only to it_callable and it_sentinel. Reworded.

it_stop_exc is not NULLed intentionally. In case of reentrant __next__ call (usually a concurrent use) we can get an exception, after the iterator was exhausted. Without it_stop_exc we cannot distinguish a StopIteration which stops iteration from StopIteration which should be converted to RuntimeError.

it_callable and it_sentinel should be NULLed because they can keep large objects, but it_stop_exc is normally just a type or a tuple of types.

Reword the aiter() documentation like the iter() one, avoid using the name
"queue" for two different things in the example, describe every field of
the iterator structs separately, add calliter_exhaust() for symmetry with
acalliter_exhaust(), use bool and designated initializers in new code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants