Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
46491ba
Initial proof-of-concept for yielding synchronous functions.
ZeroIntensity Mar 7, 2026
075a6ab
Get the parser working with "async yield from".
ZeroIntensity Mar 7, 2026
67783c0
Fix the symtable and other things.
ZeroIntensity Mar 7, 2026
fd254b4
Get things somewhat working in the eval loop.
ZeroIntensity Mar 7, 2026
a6f9c5f
Yay it mostly works.
ZeroIntensity Mar 7, 2026
c37cb05
Fix exception handling.
ZeroIntensity Mar 7, 2026
8cb5166
Support return values in async yield from.
ZeroIntensity Mar 7, 2026
17eea03
Remove tests for async gen syntax restrictions.
ZeroIntensity Mar 7, 2026
923305e
Fix more tests.
ZeroIntensity Mar 7, 2026
24603b1
Merge branch 'main' of https://github.com/python/cpython into async-y…
ZeroIntensity Mar 7, 2026
c3b318e
Fix `async yield from` on non-iterator iterable objects in async gens
johnslavik Mar 7, 2026
6145c26
Merge pull request #7 from johnslavik/hyperawait-secret-conspiracy
ZeroIntensity Mar 7, 2026
b64b563
Add tests
johnslavik Mar 9, 2026
8346a0c
Ignore `test_async_yield_from` in Ruff
johnslavik Mar 9, 2026
fa53189
Remove unnecessary `aiter()`
johnslavik Mar 9, 2026
3bfc2da
Fix commented out `test_delegating_generators_claim_to_be_running_wit…
johnslavik Mar 9, 2026
bb59a0f
Merge pull request #8 from johnslavik/hyperawait-secret-conspiracy-tests
ZeroIntensity Mar 9, 2026
6f9877e
Fix a problem in CLEANUP_ASYNC_THROW I think.
ZeroIntensity Mar 9, 2026
7d033a7
Fix a small refleak.
ZeroIntensity Mar 9, 2026
ff55764
Fix missing incref.
ZeroIntensity Mar 9, 2026
85c1087
Remove unused variable.
ZeroIntensity Mar 9, 2026
d3b6d66
Fix delegation of exceptions with athrow().
ZeroIntensity Mar 10, 2026
f0c1ecc
Temporarily skip that test.
ZeroIntensity Mar 11, 2026
bb5324c
Fix return value crash
johnslavik Mar 11, 2026
7f836e3
Merge pull request #9 from johnslavik/hyperawait-hyperfix
ZeroIntensity Mar 11, 2026
1f26722
Fix use of YieldFrom instead of AsyncYieldFrom in the AST
ZeroIntensity Mar 26, 2026
3deb42d
Correctly delegate to synchronous subgenerators.
ZeroIntensity Apr 30, 2026
35c4c23
Fix merge conflicts.
ZeroIntensity Apr 30, 2026
5d82066
Fix code generation for async yield from.
ZeroIntensity May 1, 2026
9c1e590
Fix some stack assumptions.
ZeroIntensity May 1, 2026
24a6974
Fix delegation of asend() values in async yield from.
ZeroIntensity May 1, 2026
e8d7848
Don't throw an exception for missing (synchronous) subgenerator attri…
ZeroIntensity May 1, 2026
a864e5a
Delegate attributes instead of using proxy methods.
ZeroIntensity May 2, 2026
87a47d4
Unskip several tests.
ZeroIntensity May 2, 2026
84f9706
Fix some weird edge cases with athrow().
ZeroIntensity May 2, 2026
1b7d965
Fix use of asend() instead of __anext__() for None values.
ZeroIntensity May 2, 2026
4b5e71d
Remove dead comments.
ZeroIntensity May 2, 2026
e9ac731
Adjust formatting.
ZeroIntensity May 2, 2026
e79162d
Fix the last remaining test.
ZeroIntensity May 2, 2026
92e4c77
Fix merge conflicts.
ZeroIntensity May 2, 2026
5019628
Add a whatsnew entry.
ZeroIntensity May 2, 2026
15fcd7e
Add a bunch of documentation.
ZeroIntensity May 2, 2026
43d9a7b
Cross-reference two tests suites in `test_yield_from` and `test_async…
johnslavik May 3, 2026
2129c7a
Replace "consistent with" with "analogous to"
johnslavik May 3, 2026
51405e8
Fix the C analyzer.
ZeroIntensity May 3, 2026
ef2e56a
Implement `visit_AsyncYieldFrom` in Python unparse
johnslavik May 3, 2026
18f6924
Merge pull request #11 from johnslavik/async-yield-from-fix-unparse
ZeroIntensity May 3, 2026
2d855eb
Enforce 1:1 parity between `test_async_yield_from` and `test_yield_from`
johnslavik May 3, 2026
68e47ec
Refresh `test_async_yield_from` module docstring
johnslavik May 3, 2026
14a6a78
Use async API names in test descriptions and subTests
johnslavik May 3, 2026
35fea28
Simplify cross-reference to `test_async_yield_from`
johnslavik May 3, 2026
e95df7d
Use the `sentinel` builtin for opaque test markers
johnslavik May 3, 2026
4da6ce8
Make parity-failure output more obvious
johnslavik May 3, 2026
43b6f89
Remove callable check
johnslavik May 3, 2026
b0355f9
Use fully qualified class names in parity-failure output
johnslavik May 3, 2026
c3e3329
Merge pull request #10 from johnslavik/async-yield-from-fiat-lux
ZeroIntensity May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,19 @@ The following exceptions are the exceptions that are usually raised.
Must be raised by :meth:`~object.__anext__` method of an
:term:`asynchronous iterator` object to stop the iteration.

.. attribute:: StopAsyncIteration.value

This is given as an argument when constructing the exception, and
defaults to :const:`None`. This is used for the result of
``async yield from`` expressions (see :ref:`async-yield-from`).

.. versionadded: next

.. versionadded:: 3.5

.. versionchanged:: next
Added the ``value`` attribute.

.. exception:: SyntaxError(message, details)

Raised when the parser encounters a syntax error. This may occur in an
Expand Down
93 changes: 92 additions & 1 deletion Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ Yield expressions
yield_atom: "(" `yield_expression` ")"
yield_from: "yield" "from" `expression`
yield_expression: "yield" `yield_list` | `yield_from`
async_yield_from: "async" "yield" "from" `expression`

The yield expression is used when defining a :term:`generator` function
or an :term:`asynchronous generator` function and
Expand Down Expand Up @@ -708,6 +709,10 @@ the yield expression. It can be either set explicitly when raising
.. versionchanged:: 3.3
Added ``yield from <expr>`` to delegate control flow to a subiterator.

.. versionchanged:: next
``yield from`` is now allowed to be used in an async generator.
Previously, it would raise a :class:`SyntaxError`.

The parentheses may be omitted when the yield expression is the sole expression
on the right hand side of an assignment statement.

Expand All @@ -728,6 +733,10 @@ on the right hand side of an assignment statement.
The proposal that expanded on :pep:`492` by adding generator capabilities to
coroutine functions.

:pep:`828` - Supporting ``yield from`` in asynchronous generators
The proposal that expanded on :pep:`380` by adding subgenerator delegation
to asynchronous generators.

.. index:: pair: object; generator
.. _generator-methods:

Expand Down Expand Up @@ -913,7 +922,89 @@ registered *finalizer* to be called upon finalization. For a reference example
of a *finalizer* method see the implementation of
``asyncio.Loop.shutdown_asyncgens`` in :source:`Lib/asyncio/base_events.py`.

The expression ``yield from <expr>`` is a syntax error when used in an
.. _async-yield-from:

Asynchronous ``yield from``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Conceptually, ``async yield from`` is very similar to ``yield from``, but
operates solely on asynchronous constructs rather than synchronous ones.
In particular:

.. list-table::
:widths: auto
:header-rows: 1

* * ``yield from`` construct
* ``async yield from`` construct
* * :meth:`~object.__iter__`
* :meth:`~object.__aiter__`
* * :meth:`~generator.__next__`
* :meth:`~agen.__anext__`
* * :meth:`~generator.send`
* :meth:`~agen.asend`
* * :class:`StopIteration`
* :class:`StopAsyncIteration`

To describe the above:

* The object being delegated to must be asynchronously iterable (that is, it
must implement ``__aiter__`` instead of ``__iter__``).
* When ``anext`` is called on the parent generator (the one that contains
``async yield from``), ``__anext__`` will be invoked on the subgenerator.
In contrast, a synchronous ``yield from`` would invoke ``__next__`` instead.
(Note that calling ``asend`` with a ``None`` value is equivalent to calling
``anext()``, and thus applies here.)
* All calls to ``asend``, ``athrow``, and ``aclose`` are delegated to the
subgenerator (the object returned by ``__aiter__`` in this case). This means
that a call to ``parent_generator.asend(x)`` is semantically equivalent to
``sub_generator.asend(x)``, where ``parent_generator`` is currently executing
an ``async yield from`` on ``sub_generator``.
* The result of the expression is retrieved through
:attr:`StopAsyncIteration.value` instead of :attr:`StopIteration.value`.

An example of usage for ``async yield from``:

.. code-block:: pycon

>>> import asyncio
>>> async def sleepy_count(number):
... for num in range(number):
... await asyncio.sleep(1)
... result = yield num
... print(f"Got result: {result}")
...
>>> async def counter():
... final_number = async yield from sleepy_count(5)
... yield final_number
...
>>> await anext(ag)
0
>>> await anext(ag)
Got result: None
1
>>> await ag.asend(42)
Got result: 42
2
>>> await ag.athrow(ValueError("Nobody expects the Spanish Inquisition"))
Traceback (most recent call last):
File "/home/python/cpython/Lib/concurrent/futures/_base.py", line 450, in result
return self.__get_result()
~~~~~~~~~~~~~~~~~^^
File "/home/python/cpython/Lib/concurrent/futures/_base.py", line 395, in __get_result
raise self._exception
File "<python-input-4>", line 1, in <module>
await ag.athrow(ValueError("Nobody expects the Spanish Inquisition"))
File "<python-input-0>", line 8, in counter
final_number = async yield from sleepy_count(4)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<python-input-0>", line 3, in sleepy_count
result = yield num
^^^^^^^^^
ValueError: Nobody expects the Spanish Inquisition


``async yield from`` is a :class:`SyntaxError` when used outside of an
asynchronous generator function.

.. index:: pair: object; asynchronous-generator
Expand Down
2 changes: 2 additions & 0 deletions Doc/reference/simple_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -534,11 +534,13 @@ statement. For example, the yield statements ::

yield <expr>
yield from <expr>
async yield from <expr>

are equivalent to the yield expression statements ::

(yield <expr>)
(yield from <expr>)
(async yield from <expr>)

Yield expressions and statements are only used when defining a :term:`generator`
function, and are only used in the body of the generator function. Using :keyword:`yield`
Expand Down
49 changes: 49 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Summary -- Release highlights
<whatsnew315-pybyteswriter>`
* :pep:`803`: :ref:`Stable ABI for Free-Threaded Builds <whatsnew315-abi3t>`
* :pep:`831`: :ref:`Frame pointers everywhere <whatsnew315-frame-pointers>`
* :pep:`828`: :ref:`'yield from' in async generators <whatsnew315-async-yield-from>`
* :ref:`The JIT compiler has been significantly upgraded <whatsnew315-jit>`
* :ref:`Improved error messages <whatsnew315-improved-error-messages>`
* :ref:`The official Windows 64-bit binaries now use the tail-calling interpreter
Expand Down Expand Up @@ -447,6 +448,54 @@ If not using a build tool -- or when writing such a tool -- you can select
``abi3t`` by setting the macro :c:macro:`!Py_TARGET_ABI3T` as discussed
in :ref:`abi3-compiling`.

.. _whatsnew315-async-yield-from:

:pep:`828`: Supporting ``yield from`` in asynchronous generators
----------------------------------------------------------------

Use of the :keyword:`yield from <yield>` construct and the :keyword:`return`
statement with a non-``None`` value is now allowed in an
:term:`asynchronous generator function <asynchronous generator>`. For example,
the following code would previously raise a :class:`SyntaxError`:

.. code-block:: python

async def agenerator():
yield 1
yield from range(2, 5) # Now allowed!
return 5 # Now allowed!

Additionally, there is a new ``async yield from`` statement to delegate to an
asynchronous generator:

.. code-block:: python

async def sleepy_range(start, stop):
for number in range(start, stop):
await asyncio.sleep(1)
yield number

async def agenerator():
yield 1
async yield from sleepy_range(2, 5)
yield 5


Similar to ``yield from``, ``async yield from`` may also be used as an expression,
in which case the ``return`` of the async generator is used.

For example:

.. code-block:: python

async def asubgenerator():
yield 1
yield 2
return 3

async def agenerator():
value = async yield from asubgenerator()
print(value) # 3

.. _whatsnew315-improved-error-messages:

Expand Down
3 changes: 2 additions & 1 deletion Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ simple_stmt[stmt_ty] (memo):
| &'raise' raise_stmt
| &'pass' pass_stmt
| &'del' del_stmt
| &'yield' yield_stmt
| &('yield' | 'async') yield_stmt
| &'assert' assert_stmt
| &'break' break_stmt
| &'continue' continue_stmt
Expand Down Expand Up @@ -727,6 +727,7 @@ if_expression[expr_ty]:
yield_expr[expr_ty]:
| 'yield' 'from' a=expression { _PyAST_YieldFrom(a, EXTRA) }
| 'yield' a=[star_expressions] { _PyAST_Yield(a, EXTRA) }
| 'async' 'yield' 'from' a=expression { _PyAST_AsyncYieldFrom(a, EXTRA) }

star_expressions[expr_ty]:
| a=star_expression b=(',' c=star_expression { c })+ [','] {
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ typedef struct {
PyObject *value;
} PyStopIterationObject;

typedef struct {
PyException_HEAD
PyObject *value;
} PyStopAsyncIterationObject;

typedef struct {
PyException_HEAD
PyObject *name;
Expand Down
16 changes: 11 additions & 5 deletions Include/internal/pycore_ast.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_ast_state.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_genobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);

PyAPI_FUNC(PyObject *)_PyCoro_GetAwaitableIter(PyObject *o);
PyAPI_FUNC(PyObject *)_PyAsyncGenValueWrapperNew(PyThreadState *state, PyObject *);
PyAPI_FUNC(PyObject *)_PyAsyncGenYieldFrom_New(PyThreadState *state, PyObject *);

// Exported for external JIT support
PyAPI_FUNC(PyObject *) _PyCoro_ComputeOrigin(int origin_depth, _PyInterpreterFrame *current_frame);

extern PyTypeObject _PyCoroWrapper_Type;
extern PyTypeObject _PyAsyncGenWrappedValue_Type;
extern PyTypeObject _PyAsyncGenAThrow_Type;
extern PyTypeObject _PyAsyncGenYieldFrom_Type;

#ifdef __cplusplus
}
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ struct _py_func_state {
If you add a new static type to the standard library, you may have to
update one of these numbers.
*/
#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 120
#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 121
#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES \
(_Py_NUM_MANAGED_PREINITIALIZED_TYPES + 83)
#define _Py_MAX_MANAGED_STATIC_EXT_TYPES 10
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_intrinsics.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
#define INTRINSIC_TYPEVARTUPLE 9
#define INTRINSIC_SUBSCRIPT_GENERIC 10
#define INTRINSIC_TYPEALIAS 11
#define INSTRINSIC_ASYNC_GEN_WRAP_YIELD_FROM 12

#define MAX_INTRINSIC_1 11
#define MAX_INTRINSIC_1 12


/* Binary Functions: */
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_magic_number.h
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ Known values:
Python 3.15a8 3663 (Merge GET_ITER and GET_YIELD_FROM_ITER. Modify SEND to make it a bit more like FOR_ITER)
Python 3.15a8 3664 (Fix __qualname__ for __annotate__ functions)
Python 3.15a8 3665 (Add FOR_ITER_VIRTUAL and GET_ITER specializations)
Python 3.15a6 3666 (PEP 828: yield from for asyncgens)


Python 3.16 will start with 3700
Expand All @@ -309,7 +310,7 @@ PC/launcher.c must also be updated.

*/

#define PYC_MAGIC_NUMBER 3665
#define PYC_MAGIC_NUMBER 3666
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
(little-endian) and then appending b'\r\n'. */
#define PYC_MAGIC_NUMBER_TOKEN \
Expand Down
Loading
Loading