https://github.com/python/cpython/commit/ac430e328525dd803053dea05bda9701a4a651c2
commit: ac430e328525dd803053dea05bda9701a4a651c2
branch: main
author: Shamil <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-22T14:18:28+05:30
summary:

gh-158304: Avoid creating StopIteration for every value yielded by an async 
generator (#158315)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-10-12-25.gh-issue-158304.3pdcnJ.rst
M Objects/genobject.c

diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-10-12-25.gh-issue-158304.3pdcnJ.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-10-12-25.gh-issue-158304.3pdcnJ.rst
new file mode 100644
index 00000000000000..394367d0dba8eb
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-28-10-12-25.gh-issue-158304.3pdcnJ.rst
@@ -0,0 +1,4 @@
+Speed up iteration over asynchronous generators: values yielded to
+``async for`` no longer create and discard a :exc:`StopIteration` exception
+at every nesting level. The ``async_generators`` pyperformance benchmark is
+about 30% faster. Patch by Shamil Abdulaev.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 05a331e88b88c9..1c95a569af8b9b 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -1965,8 +1965,10 @@ PyAsyncGen_New(PyFrameObject *f, PyObject *name, 
PyObject *qualname)
     return (PyObject*)ag;
 }
 
-static PyObject *
-async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject *result)
+// Report an async yield as PYGEN_RETURN with the yielded value.
+static PySendResult
+async_gen_unwrap_send(PyAsyncGenObject *gen, PyObject *result,
+                      PyObject **presult)
 {
     if (result == NULL) {
         if (!PyErr_Occurred()) {
@@ -1979,17 +1981,31 @@ async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject 
*result)
             FT_ATOMIC_STORE_INT8_RELAXED(gen->ag_closed, 1);
         }
 
-        return NULL;
+        *presult = NULL;
+        return PYGEN_ERROR;
     }
 
     if (_PyAsyncGenWrappedValue_CheckExact(result)) {
         /* async yield */
-        
_PyGen_SetStopIterationValue(((_PyAsyncGenWrappedValue*)result)->agw_val);
+        *presult = Py_NewRef(((_PyAsyncGenWrappedValue*)result)->agw_val);
         Py_DECREF(result);
-        return NULL;
+        return PYGEN_RETURN;
     }
 
-    return result;
+    *presult = result;
+    return PYGEN_NEXT;
+}
+
+static PyObject *
+async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject *result)
+{
+    PyObject *value;
+    if (async_gen_unwrap_send(gen, result, &value) == PYGEN_RETURN) {
+        _PyGen_SetStopIterationValue(value);
+        Py_DECREF(value);
+        return NULL;
+    }
+    return value;
 }
 
 
@@ -2002,7 +2018,10 @@ async_gen_asend_dealloc(PyObject *self)
     assert(PyAsyncGenASend_CheckExact(self));
     PyAsyncGenASend *ags = _PyAsyncGenASend_CAST(self);
 
-    if (PyObject_CallFinalizerFromDealloc(self)) {
+    // The finalizer only warns about an asend() that was never awaited.
+    if (ags->ags_state == AWAITABLE_STATE_INIT
+        && PyObject_CallFinalizerFromDealloc(self))
+    {
         return;
     }
 
@@ -2025,18 +2044,19 @@ async_gen_asend_traverse(PyObject *self, visitproc 
visit, void *arg)
 }
 
 
-static PyObject *
-async_gen_asend_send(PyObject *self, PyObject *arg)
+PySendResult
+_PyAsyncGenASend_Send(PyObject *self, PyObject *arg, PyObject **presult)
 {
     PyAsyncGenASend *o = _PyAsyncGenASend_CAST(self);
 
+    *presult = NULL;
     int8_t state = FT_ATOMIC_LOAD_INT8_RELAXED(o->ags_state);
     do {
         if (state == AWAITABLE_STATE_CLOSED) {
             PyErr_SetString(
                 PyExc_RuntimeError,
                 "cannot reuse already awaited __anext__()/asend()");
-            return NULL;
+            return PYGEN_ERROR;
         }
         if (state == AWAITABLE_STATE_ITER) {
             goto do_send;
@@ -2053,7 +2073,7 @@ async_gen_asend_send(PyObject *self, PyObject *arg)
         PyErr_SetString(
             PyExc_RuntimeError,
             "anext(): asynchronous generator is already running");
-        return NULL;
+        return PYGEN_ERROR;
     }
 
     if (arg == NULL || arg == Py_None) {
@@ -2061,29 +2081,29 @@ async_gen_asend_send(PyObject *self, PyObject *arg)
     }
 
     PyObject *result;
+    PySendResult res;
 do_send:
     result = gen_send((PyObject*)o->ags_gen, arg);
-    result = async_gen_unwrap_value(o->ags_gen, result);
+    res = async_gen_unwrap_send(o->ags_gen, result, presult);
 
-    if (result == NULL) {
+    if (res != PYGEN_NEXT) {
         FT_ATOMIC_STORE_INT8_RELAXED(o->ags_state, AWAITABLE_STATE_CLOSED);
         FT_ATOMIC_STORE_INT8_RELEASE(o->ags_gen->ag_running_async, 0);
     }
 
-    return result;
+    return res;
 }
 
-PySendResult
-_PyAsyncGenASend_Send(PyObject *iter, PyObject *arg, PyObject **result)
+static PyObject *
+async_gen_asend_send(PyObject *self, PyObject *arg)
 {
-    *result = async_gen_asend_send(iter, arg);
-    if (*result != NULL) {
-        return PYGEN_NEXT;
-    }
-    if (_PyGen_FetchStopIterationValue(result) == 0) {
-        return PYGEN_RETURN;
+    PyObject *result;
+    if (_PyAsyncGenASend_Send(self, arg, &result) == PYGEN_RETURN) {
+        _PyGen_SetStopIterationValue(result);
+        Py_DECREF(result);
+        return NULL;
     }
-    return PYGEN_ERROR;
+    return result;
 }
 
 
@@ -2168,6 +2188,7 @@ static void
 async_gen_asend_finalize(PyObject *self)
 {
     PyAsyncGenASend *ags = _PyAsyncGenASend_CAST(self);
+    // async_gen_asend_dealloc() calls this only in the INIT state.
     if (ags->ags_state == AWAITABLE_STATE_INIT) {
         _PyErr_WarnUnawaitedAgenMethod(ags->ags_gen, &_Py_ID(asend));
     }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to