Given this simple snippet, why does the first anext()
call work
but the second anext()
results in a strange error?
Reading the documentation of anext()
, I would assume that the supplied default
would be used when there is no “next” value available:
If default is given, it is returned if the iterator is exhausted, otherwise
StopAsyncIteration
is raised.
Running this in an (interactive) Python 3.12 interpreter on MacOS Apple M3 Pro,
import asyncio
async def generator(it=None):
if it is not None:
yield (it, it)
async def my_func():
# results in a=1 b=1
a, b = await anext(generator(1), (2, 3))
# results in no printing
async for a, b in generator():
print(a, b)
# raises exception
a, b = await anext(generator(), (2, 3))
loop = asyncio.new_event_loop()
loop.run_until_complete(my_func())
results in this exception:
StopAsyncIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/manuel/.pyenv/versions/3.12.0/lib/python3.12/asyncio/base_events.py", line 664, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<stdin>", line 6, in my_func
SystemError: <class 'StopIteration'> returned a result with an exception set
My snippet fails in Python 3.11 & 3.13 too with the same exception message.
1
This is a Python bug, and should be reported.
When the anext
-with-default-value implementation hits StopAsyncIteration
, it calls _PyGen_SetStopIterationValue
to replace the exception with a StopIteration
. I’m not quite sure whether this is happening in __next__
or send
, but both methods do essentially the same thing in this case. Here’s __next__
‘s handling:
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
_PyGen_SetStopIterationValue(obj->default_value);
}
_PyGen_SetStopIterationValue
looks like this:
int
_PyGen_SetStopIterationValue(PyObject *value)
{
PyObject *e;
if (value == NULL ||
(!PyTuple_Check(value) && !PyExceptionInstance_Check(value)))
{
/* Delay exception instantiation if we can */
PyErr_SetObject(PyExc_StopIteration, value);
return 0;
}
/* Construct an exception instance manually with
* PyObject_CallOneArg and pass it to PyErr_SetObject.
*
* We do this to handle a situation when "value" is a tuple, in which
* case PyErr_SetObject would set the value of StopIteration to
* the first element of the tuple.
*
* (See PyErr_SetObject/_PyErr_CreateException code for details.)
*/
e = PyObject_CallOneArg(PyExc_StopIteration, value);
if (e == NULL) {
return -1;
}
PyErr_SetObject(PyExc_StopIteration, e);
Py_DECREF(e);
return 0;
}
This function needs to perform special handling when the argument is a tuple or an exception object. In your case, the argument is a tuple (2, 3)
. But the special handling has an issue.
There is already an active exception set, the StopAsyncIteration
. It is not safe to call arbitrary Python APIs with an exception set. All the calls in the usual-case handling are okay, but this line in the special-case handling:
e = PyObject_CallOneArg(PyExc_StopIteration, value);
is unsafe. Python detects an inconsistent state, resulting in the error you see.
To fix this issue, either _PyGen_SetStopIterationValue
or the code that calls it should use PyErr_Clear
to cancel the current exception before the PyObject_CallOneArg(PyExc_StopIteration, value)
call happens.
2
i solved the issue by replacing the tuple in the default parameter with a list, so [2,3] in stead of (2,3).
i cannot tell you why it doesn’t work with a tuple, but this works for me.
a, b = await anext(generator(), [2, 3])
4