Skip to content

Commit bef00e0

Browse files
committed
Initial revision.
1 parent 02e54b1 commit bef00e0

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed

peps/pep-9999.rst

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
PEP: 9999
2+
Title: Supporting 'yield from' in asynchronous generators
3+
Author: Peter Bierma <peter@python.org>
4+
Discussions-To: Pending
5+
Status: Draft
6+
Type: Standards Track
7+
Created: 07-Mar-2026
8+
Python-Version: 3.15
9+
Post-History: Pending
10+
11+
12+
Abstract
13+
========
14+
15+
This PEP introduces support for :keyword:`yield from <yield>` in an
16+
:ref:`asynchronous generator function <asynchronous-generator-functions>`.
17+
18+
For example, the following code is valid under this PEP:
19+
20+
.. code-block:: python
21+
22+
def generator():
23+
yield 1
24+
yield 2
25+
26+
async def main():
27+
yield from generator()
28+
29+
30+
31+
In addition, this PEP introduces a new ``async yield from`` syntax to use
32+
existing ``yield from`` semantics on an asynchronous generator:
33+
34+
.. code-block:: python
35+
36+
async def agenerator():
37+
yield 1
38+
yield 2
39+
40+
async def main():
41+
async yield from agenerator()
42+
43+
44+
In order to allow use of ``async yield from`` as an expression, this PEP
45+
removes the existing limitation that asynchronous generators may not return
46+
a non-``None`` value. For example, the following code is valid under this
47+
proposal:
48+
49+
.. code-block:: python
50+
51+
async def agenerator():
52+
yield 1
53+
return 2
54+
55+
async def main():
56+
result = (async yield from agenerator())
57+
assert result == 2
58+
59+
60+
Terminology
61+
===========
62+
63+
This PEP refers to an ``async def`` function that contains a ``yield``
64+
as an :term:`asynchronous generator`, sometimes suffixed with "function".
65+
66+
In contrast, the object returned by an asynchronous generator is referred to
67+
as an :term:`asynchronous generator iterator` in this PEP.
68+
69+
70+
Motivation
71+
==========
72+
73+
74+
Implementation complexity has gone down
75+
---------------------------------------
76+
77+
Historically, ``yield from`` was not added to asynchronous generators due to
78+
concerns about the complexity of the implementation. To quote :pep:`525`:
79+
80+
While it is theoretically possible to implement yield from support for
81+
asynchronous generators, it would require a serious redesign of the
82+
generators implementation.
83+
84+
As of March 2026, the author of this proposal does not believe this to be true
85+
given the current state of CPython's asynchronous generator implementation.
86+
This proposal comes with a reference implementation to argue this point, but
87+
it is acknowledged that complexity is often subjective.
88+
89+
90+
Symmetry with synchronous generators
91+
------------------------------------
92+
93+
``yield from`` was added to synchronous generators in :pep:`380` because
94+
delegation to another generator is a useful thing to do. Due to the
95+
aforementioned complexity in CPython's generator implementation, PEP 525
96+
omitted support for ``yield from`` in asynchronous generators, but this has
97+
left a gap in the language.
98+
99+
This gap has not gone unnoticed by users. There have been three separate
100+
requests for ``yield from`` or ``return`` behavior (which are closely related)
101+
in asynchronous generators:
102+
103+
1. https://discuss.python.org/t/8897
104+
2. https://discuss.python.org/t/47050
105+
3. https://discuss.python.org/t/66886
106+
107+
Additionally, this design flaw has `come up
108+
<https://stackoverflow.com/questions/47376408>`_ on StackOverflow.
109+
110+
111+
Subgenerator delegation is useful for asynchronous generators
112+
-------------------------------------------------------------
113+
114+
The current workaround for the lack of ``yield from`` support in asynchronous
115+
generators is to use a ``for``/``async for`` loop that manually yields each
116+
item. This comes with a few drawbacks:
117+
118+
1. It obscures the intent of the code and increases the amount of effort
119+
necessary to work with asynchronous generators, because each delegation
120+
point becomes a loop. This damages the power of asynchronous generators.
121+
2. :meth:`~agend.asend`, :meth:`~agen.athrow`, and :meth:`~agen.aclose`,
122+
do not interact properly with the caller. This is the primary reason that
123+
``yield from`` was added in the first place.
124+
3. Return values are not natively supported with asynchronous generators. The
125+
workaround for this it to raise an exception, which increases boilerplate.
126+
127+
128+
Specification
129+
=============
130+
131+
Syntax
132+
------
133+
134+
135+
Compiler changes
136+
^^^^^^^^^^^^^^^^
137+
138+
The compiler will no longer emit a :exc:`SyntaxError` for
139+
:keyword:`yield from <yield>` and :keyword:`return` statements inside
140+
asynchronous generators.
141+
142+
143+
Grammar changes
144+
^^^^^^^^^^^^^^^
145+
146+
The ``yield_expr`` and ``simple_stmt`` rules need to be updated for the new
147+
``async yield from`` syntax:
148+
149+
.. code-block::
150+
151+
yield_expr[expr_ty]:
152+
| 'async' 'yield' 'from' a=expression
153+
154+
simple_stmt[stmt_ty] (memo):
155+
| &('yield' | 'async') yield_stmt
156+
157+
158+
``yield from`` semantics
159+
------------------------
160+
161+
This PEP retains all existing ``yield from`` semantics; the only detail is
162+
that asynchronous generators may now use it.
163+
164+
Because the existing ``yield from`` behavior may only yield from a synchronous
165+
generator, this is true for asynchronous generators as well.
166+
167+
For example:
168+
169+
.. code-block:: python
170+
171+
def generator():
172+
yield 1
173+
yield 2
174+
yield 3
175+
176+
async def main():
177+
yield from generator()
178+
yield 4
179+
180+
In the above code, ``main`` will yield ``1``, ``2``, ``3``, ``4``.
181+
All subgenerator delegation semantics are retained.
182+
183+
184+
``async yield from`` base semantics
185+
-----------------------------------
186+
187+
``async yield from`` is equivalent to ``yield from``, with the exception that:
188+
189+
1. :meth:`~object.__aiter__` is called to retrieve the asynchronous
190+
generator iterator.
191+
2. :meth:`~object.__asend__` is called to advance the asynchronous generator.
192+
193+
``async yield from`` is only allowed in an asynchronous generator function;
194+
using it elsewhere will raise a :exc:`SyntaxError`.
195+
196+
In an asynchronous generator, ``async yield from`` is conceptually equivalent to:
197+
198+
.. code-block:: python
199+
200+
async for item in agenerator():
201+
yield item
202+
203+
``async yield from`` retains all the subgenerator delegation behavior present
204+
in standard ``yield from`` expressions. This behavior is outlined in :pep:`380`
205+
and :ref:`the documentation <yieldexpr>`. In short, values passed with
206+
:meth:`~generator.send` and exceptions supplied with :meth:`~generator.throw`
207+
are also passed to the target generator.
208+
209+
210+
``async yield from`` as an expression
211+
-------------------------------------
212+
213+
``async yield from`` may also be used as an expression. For reference,
214+
the result of a ``yield from`` expression is the object returned by the
215+
synchronous generator. ``async yield from`` does the same; the expression
216+
value is the value returned by the executed asynchronous generator.
217+
218+
However, Python currently prevents asynchronous generators from returning
219+
any non-``None`` value. This limitation is removed by this PEP.
220+
221+
When an asynchronous generator iterator is exhausted, it will raise a
222+
:exc:`StopAsyncIteration` exception with a ``value`` attribute, similar
223+
to the existing :exc:`StopIteration` behavior with synchronous generators.
224+
To visualize:
225+
226+
.. code-block:: python
227+
228+
async def agenerator():
229+
yield 1
230+
return 2
231+
232+
async def main():
233+
gen = agenerator()
234+
print(await gen.asend(None)) # 1
235+
try:
236+
await gen.asend(None)
237+
except StopAsyncIteration as result:
238+
print(result.value) # 2
239+
240+
241+
The contents of the ``value`` attribute will be the result of the ``async
242+
yield from`` expression.
243+
244+
For example:
245+
246+
.. code-block:: python
247+
248+
async def agenerator():
249+
yield 1
250+
return 2
251+
252+
async def main():
253+
result = (async yield from agenerator())
254+
print(result) # 2
255+
256+
257+
Rationale
258+
=========
259+
260+
The distinction between ``yield from`` and ``async yield from`` in this proposal
261+
is consistent with existing asynchronous syntax constructs in Python.
262+
For example, there are two constructs for context managers: ``with`` and
263+
``async with``.
264+
265+
This PEP follows this pattern; ``yield from`` continues to be synchronous, even
266+
in asynchronous generators, and ``async yield from`` is the asynchronous
267+
variation.
268+
269+
270+
Backwards Compatibility
271+
=======================
272+
273+
This PEP introduces a backwards-compatible syntax change.
274+
275+
276+
Security Implications
277+
=====================
278+
279+
This PEP has no known security implications.
280+
281+
282+
How to Teach This
283+
=================
284+
285+
The details of this proposal will be located in Python's canonical
286+
documentation, as with all other language constructs. However, this PEP
287+
intends to be very intuitive; users should be able to deduce the behavior of
288+
``yield from`` in an asynchronous generator based on their own background
289+
knowledge of ``yield from`` in synchronous generators.
290+
291+
292+
Reference Implementation
293+
========================
294+
295+
A reference implementation of this branch can be found
296+
`here <https://github.com/python/cpython/compare/main...zerointensity:cpython:async-yield-from>`_.
297+
298+
299+
Rejected Ideas
300+
==============
301+
302+
TBD.
303+
304+
305+
Acknowledgements
306+
================
307+
308+
Thanks to Bartosz Sławecki for aiding in the development of the reference
309+
implementation of this PEP.
310+
311+
In the reference implementation of this proposal, the :exc:`StopAsyncIteration`
312+
changes in addition to the support for non-``None`` return values inside
313+
asynchronous generators were largely based on Alex Dixon's design from
314+
`python/cpython#125401 <https://github.com/python/cpython/pull/125401>`_
315+
316+
317+
Change History
318+
==============
319+
320+
TBD.
321+
322+
323+
Copyright
324+
=========
325+
326+
This document is placed in the public domain or under the
327+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)