PEP 828 – Supporting ‘yield from’ in asynchronous generators
- Author:
- Peter Bierma <peter at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 07-Mar-2026
- Python-Version:
- 3.15
- Post-History:
- 07-Mar-2026, 09-Mar-2026
Abstract
This PEP introduces support for yield from in an
asynchronous generator function.
For example, the following code is valid under this PEP:
def generator():
yield 1
yield 2
async def main():
yield from generator()
In addition, this PEP introduces a new async yield from construct to
delegate to an asynchronous generator:
async def agenerator():
yield 1
yield 2
async def main():
async yield from agenerator()
In order to allow use of async yield from as an expression, this PEP
removes the existing limitation that asynchronous generators may not return
a non-None value. For example, the following code is valid under this
proposal:
async def agenerator():
yield 1
return 2
async def main():
result = async yield from agenerator()
assert result == 2
Terminology
This PEP refers to an async def function that contains a yield
as an asynchronous generator, sometimes suffixed with “function”.
In contrast, the object returned by an asynchronous generator is referred to as an asynchronous generator iterator in this PEP.
This PEP also uses the term “subgenerator” to refer to a generator, synchronous
or asynchronous, that is used inside of a yield from or async yield from
expression.
Motivation
Implementation complexity has gone down
Historically, yield from was not added to asynchronous generators due to
concerns about the complexity of the implementation. To quote PEP 525:
While it is theoretically possible to implementyield fromsupport for asynchronous generators, it would require a serious redesign of the generators implementation.
As of March 2026, the author of this proposal does not believe this to be true given the current state of CPython’s asynchronous generator implementation. This proposal comes with a reference implementation to argue this point, but it is acknowledged that complexity is often subjective.
Symmetry with synchronous generators
yield from was added to synchronous generators in PEP 380 because
delegation to another generator is a useful thing to do. Due to the
aforementioned complexity in CPython’s generator implementation, PEP 525
omitted support for yield from in asynchronous generators, but this has
left a gap in the language.
This gap has not gone unnoticed by users. There have been three separate
requests for yield from or return behavior (which are closely related)
in asynchronous generators:
- https://discuss.python.org/t/8897
- https://discuss.python.org/t/47050
- https://discuss.python.org/t/66886
Additionally, users have questioned this design decision on Stack Overflow.
Subgenerator delegation is useful for asynchronous generators
The current workaround for the lack of yield from support in asynchronous
generators is to use a for/async for loop that manually yields each
item. This comes with a few drawbacks:
- It obscures the intent of the code and increases the amount of effort necessary to work with asynchronous generators, because each delegation point becomes a loop. This damages the power of asynchronous generators.
asend(),athrow(), andaclose()do not interact properly with the caller. This is the primary reason thatyield fromwas added in the first place.- Return values are not natively supported with asynchronous generators. The workaround for this is to raise an exception, which increases boilerplate.
Specification
Syntax
Compiler changes
The compiler will no longer emit a SyntaxError for
yield from and return statements inside
asynchronous generators.
Grammar changes
The yield_expr and simple_stmt rules need to be updated for the new
async yield from syntax:
yield_expr[expr_ty]:
| 'async' 'yield' 'from' a=expression
simple_stmt[stmt_ty] (memo):
| &('yield' | 'async') yield_stmt
yield from behavior in asynchronous generators
This PEP retains all existing yield from semantics; the only detail is
that asynchronous generators may now use it.
Because the existing yield from behavior may only yield from a synchronous
subgenerator, this is true for asynchronous generators as well.
For example:
def generator():
yield 1
yield 2
yield 3
async def main():
yield from generator()
yield 4
In the above code, main will yield 1, 2, 3, 4.
All subgenerator delegation semantics are retained.
async yield from as a statement
async yield from is equivalent to yield from, with the exception that:
__aiter__()is called to retrieve the asynchronous generator iterator.asend()is called to advance the asynchronous generator iterator.
async yield from is only allowed in an asynchronous generator function;
using it elsewhere will raise a SyntaxError.
In an asynchronous generator, async yield from is conceptually equivalent to:
async for item in agenerator():
yield item
async yield from retains all the subgenerator delegation behavior present
in standard yield from expressions. This behavior is outlined in PEP 380
and the documentation. In short, values passed with
asend() and exceptions supplied with athrow()
are also passed to the target generator.
async yield from as an expression
async yield from may also be used as an expression. For reference,
the result of a yield from expression is the object returned by the
synchronous generator. async yield from does the same; the expression
value is the value returned by the executed asynchronous generator.
However, Python currently prevents asynchronous generators from returning
any non-None value. This limitation is removed by this PEP.
When an asynchronous generator iterator is exhausted, it will raise a
StopAsyncIteration exception with a value attribute, similar
to the existing StopIteration behavior with synchronous generators.
To visualize:
async def agenerator():
yield 1
return 2
async def main():
gen = agenerator()
print(await gen.asend(None)) # 1
try:
await gen.asend(None)
except StopAsyncIteration as result:
print(result.value) # 2
The contents of the value attribute will be the result of the async
yield from expression.
For example:
async def agenerator():
yield 1
return 2
async def main():
result = async yield from agenerator()
print(result) # 2
Rationale
The distinction between yield from and async yield from in this proposal
is consistent with existing asynchronous syntax constructs in Python.
For example, there are two constructs for context managers: with and
async with.
This PEP follows this pattern; yield from continues to be synchronous, even
in asynchronous generators, and async yield from is the asynchronous
variation.
Backwards Compatibility
This PEP introduces a backwards-compatible syntax change.
Security Implications
This PEP has no known security implications.
How to Teach This
The details of this proposal will be located in Python’s canonical
documentation, as with all other language constructs. However, this PEP
intends to be very intuitive; users should be able to deduce the behavior of
yield from in an asynchronous generator based on their own background
knowledge of yield from in synchronous generators.
Potential footguns
Forgetting to await a future
In asyncio, a future object is natively
iterable. This means that if one were trying to iterate over the result of a
future, forgetting to await the future may accidentally await the
future itself, leading to a spurious error.
For example:
import asyncio
async def steps():
await asyncio.sleep(0.25)
await asyncio.sleep(0.25)
await asyncio.sleep(0.25)
return [1, 2, 3]
async def agenerator():
# Forgot to await!
yield from asyncio.ensure_future(steps())
async def run():
total = 0
async for i in agenerator():
# TypeError?!
total += i
print(total)
Attempting to use yield from on an asynchronous subgenerator
A common intuition among developers is that yield from inside an
asynchronous generator will also delegate to another asynchronous generator.
As such, many users were surprised to see that, in this proposal, the following
code is invalid:
async def asubgenerator():
yield 1
yield 2
async def agenerator():
yield from asubgenerator()
As a solution, when yield from is given an object that is not iterable,
the implementation can detect if that object is asynchronously iterable.
If it is, async yield from can be suggested in the exception message.
This is done in the reference implementation of this proposal; the example
above raises a TypeError that reads async_generator object is not
iterable. Did you mean 'async yield from'?
Reference Implementation
A reference implementation of this PEP can be found at python/cpython#145716.
Rejected Ideas
Using yield from to delegate to asynchronous generators
It has been argued that many developers may intuitively believe that using a
plain yield from inside an asynchronous generator would also delegate to
an asynchronous subgenerator rather than a synchronous subgenerator. As such,
it was proposed to make yield from always delegate to an asynchronous
subgenerator.
For example:
async def asubgenerator():
yield 1
yield 2
async def agenerator():
yield from asubgenerator()
This was rejected, primarily because it felt very wrong for yield from x to
be valid or invalid depending on the type of generator it was used in.
In addition, there is no precedent for this kind of behavior in Python; inherently
synchronous constructs always have an asynchronous counterpart for use in
asynchronous functions, instead of implicitly switching protocols depending on
the type of function it is used in. For example, with always means that the
synchronous context management protocol will
be invoked, even when used in an async def function.
Finally, this would leave a gap in asynchronous generators, because there would be
no mechanism for delegating to a synchronous subgenerator. Even if this is not a
common pattern today, this may become common in the future, in which case it would
be very difficult to change the meaning of yield from in an asynchronous
generator.
Letting yield from determine which protocol to use
As a solution to the above rejected idea, it was proposed to allow yield from x
to invoke the synchronous or asynchronous generator protocol depending on the type
of x. In turn, this would allow developers to delegate to both synchronous
and asynchronous subgenerators while continuing to use the familiar yield from
syntax.
For example:
async def asubgenerator():
yield 1
yield 2
async def agenerator():
yield from asubgenerator()
yield from range(3, 5)
Mechanically, this is possible, but the exact behavior will likely be counterintuitive and ambigious. In particular:
- If an object implements both
__iter__()and__aiter__(), it’s not clear which protocol Python should choose. - If the chosen protocol raises an exception, should the exception be propagated, or should Python try to use the other protocol first?
Additionally, this approach is inherently slower, because of the additional overhead of detecting which generator protocol to use.
Acknowledgements
Thanks to Bartosz Sławecki for aiding in the development of the reference
implementation of this PEP. In addition, the StopAsyncIteration
changes alongside the support for non-None return values inside
asynchronous generators were largely based on Alex Dixon’s design from
python/cpython#125401.
Change History
TBD.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0828.rst
Last modified: 2026-03-17 18:19:44 GMT