Skip to content

Commit 863542d

Browse files
ZeroIntensitybrianschubertJelleZijlstrahugovk
authored
PEP 828: Supporting 'yield from' in asynchronous generators (#4854)
Co-authored-by: Brian Schubert <brianm.schubert@gmail.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 4eaa084 commit 863542d

File tree

2 files changed

+362
-0
lines changed

2 files changed

+362
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,7 @@ peps/pep-0822.rst @methane
701701
peps/pep-0825.rst @warsaw @dstufft
702702
peps/pep-0826.rst @savannahostrowski
703703
peps/pep-0827.rst @1st1
704+
peps/pep-0828.rst @ZeroIntensity
704705
# ...
705706
peps/pep-2026.rst @hugovk
706707
# ...

peps/pep-0828.rst

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
PEP: 828
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: `07-Mar-2026 <https://discuss.python.org/t/106430>`__
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 decision has `come up
108+
<https://stackoverflow.com/questions/47376408>`__ on Stack Overflow.
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:`~agen.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:: peg
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`` behavior in asynchronous generators
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`` as a statement
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:`~agen.asend` is called to advance the asynchronous generator
192+
iterator.
193+
194+
``async yield from`` is only allowed in an asynchronous generator function;
195+
using it elsewhere will raise a :exc:`SyntaxError`.
196+
197+
In an asynchronous generator, ``async yield from`` is conceptually equivalent to:
198+
199+
.. code-block:: python
200+
201+
async for item in agenerator():
202+
yield item
203+
204+
``async yield from`` retains all the subgenerator delegation behavior present
205+
in standard ``yield from`` expressions. This behavior is outlined in :pep:`380`
206+
and :ref:`the documentation <yieldexpr>`. In short, values passed with
207+
:meth:`~agen.asend` and exceptions supplied with :meth:`~agen.athrow`
208+
are also passed to the target generator.
209+
210+
211+
``async yield from`` as an expression
212+
-------------------------------------
213+
214+
``async yield from`` may also be used as an expression. For reference,
215+
the result of a ``yield from`` expression is the object returned by the
216+
synchronous generator. ``async yield from`` does the same; the expression
217+
value is the value returned by the executed asynchronous generator.
218+
219+
However, Python currently prevents asynchronous generators from returning
220+
any non-``None`` value. This limitation is removed by this PEP.
221+
222+
When an asynchronous generator iterator is exhausted, it will raise a
223+
:exc:`StopAsyncIteration` exception with a ``value`` attribute, similar
224+
to the existing :exc:`StopIteration` behavior with synchronous generators.
225+
To visualize:
226+
227+
.. code-block:: python
228+
229+
async def agenerator():
230+
yield 1
231+
return 2
232+
233+
async def main():
234+
gen = agenerator()
235+
print(await gen.asend(None)) # 1
236+
try:
237+
await gen.asend(None)
238+
except StopAsyncIteration as result:
239+
print(result.value) # 2
240+
241+
242+
The contents of the ``value`` attribute will be the result of the ``async
243+
yield from`` expression.
244+
245+
For example:
246+
247+
.. code-block:: python
248+
249+
async def agenerator():
250+
yield 1
251+
return 2
252+
253+
async def main():
254+
result = async yield from agenerator()
255+
print(result) # 2
256+
257+
258+
Rationale
259+
=========
260+
261+
The distinction between ``yield from`` and ``async yield from`` in this proposal
262+
is consistent with existing asynchronous syntax constructs in Python.
263+
For example, there are two constructs for context managers: ``with`` and
264+
``async with``.
265+
266+
This PEP follows this pattern; ``yield from`` continues to be synchronous, even
267+
in asynchronous generators, and ``async yield from`` is the asynchronous
268+
variation.
269+
270+
271+
Backwards Compatibility
272+
=======================
273+
274+
This PEP introduces a backwards-compatible syntax change.
275+
276+
277+
Security Implications
278+
=====================
279+
280+
This PEP has no known security implications.
281+
282+
283+
How to Teach This
284+
=================
285+
286+
The details of this proposal will be located in Python's canonical
287+
documentation, as with all other language constructs. However, this PEP
288+
intends to be very intuitive; users should be able to deduce the behavior of
289+
``yield from`` in an asynchronous generator based on their own background
290+
knowledge of ``yield from`` in synchronous generators.
291+
292+
293+
Potential footguns
294+
------------------
295+
296+
Forgetting to ``await`` a future
297+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
298+
299+
In :mod:`asyncio`, a :ref:`future <asyncio-future-obj>` object is natively
300+
iterable. This means that if one were trying to iterate over the result of a
301+
future, forgetting to :keyword:`await` the future may accidentally await the
302+
future itself, leading to a spurious error.
303+
304+
For example:
305+
306+
.. code-block:: python
307+
308+
import asyncio
309+
310+
async def steps():
311+
await asyncio.sleep(0.25)
312+
await asyncio.sleep(0.25)
313+
await asyncio.sleep(0.25)
314+
return [1, 2, 3]
315+
316+
async def generator():
317+
# Forgot to await!
318+
yield from asyncio.ensure_future(steps())
319+
320+
async def run():
321+
total = 0
322+
async for i in generator():
323+
# TypeError?!
324+
total += i
325+
print(total)
326+
327+
328+
Reference Implementation
329+
========================
330+
331+
A reference implementation of this PEP can be found
332+
`here <https://github.com/python/cpython/compare/main...zerointensity:cpython:async-yield-from>`__.
333+
334+
335+
Rejected Ideas
336+
==============
337+
338+
TBD.
339+
340+
341+
Acknowledgements
342+
================
343+
344+
Thanks to Bartosz Sławecki for aiding in the development of the reference
345+
implementation of this PEP. In addition, the :exc:`StopAsyncIteration`
346+
changes in addition to the support for non-``None`` return values inside
347+
asynchronous generators were largely based on Alex Dixon's design from
348+
`python/cpython#125401 <https://github.com/python/cpython/pull/125401>`__
349+
350+
351+
Change History
352+
==============
353+
354+
TBD.
355+
356+
357+
Copyright
358+
=========
359+
360+
This document is placed in the public domain or under the
361+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)