PEP 828 – Supporting ‘yield from’ in asynchronous generators
- Author:
- Peter Bierma <peter at python.org>
- PEP-Delegate:
- Yury Selivanov <yury at vercel.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 07-Mar-2026
- Python-Version:
- 3.16
- Post-History:
- 07-Mar-2026, 09-Mar-2026
Abstract
This PEP introduces support for yield from in an
asynchronous generator function:
async def agenerator():
yield 1
yield 2
return 3
async def main():
result = yield from agenerator()
assert result == 3
Terminology
This PEP refers to an async def function that contains a yield
as an asynchronous generator, sometimes suffixed with “function”.
This is not to be confused with an asynchronous generator iterator,
which is the object returned by an asynchronous generator.
This PEP also uses the term “subgenerator” to refer to a generator, synchronous
or asynchronous, that is used inside a yield from.
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
Compiler changes
The compiler will no longer emit a SyntaxError for
return or yield from statements inside asynchronous generators.
Changes to StopAsyncIteration
The StopAsyncIteration exception will gain a new value attribute
to be used as the result of yield from expressions in asynchronous generators.
This attribute can be supplied by passing a positional argument to
StopAsyncIteration. For example:
>>> exception = StopAsyncIteration(42)
>>> exception.value
42
If no argument is supplied, value will be None.
return statements inside asynchronous generators
In the body of an asynchronous generator function, the statement
return expression is roughly equivalent to
raise StopAsyncIteration(expression). However, similar to implicit
StopIteration exceptions raised inside synchronous generators,
the exception cannot be caught in the body of the asynchronous generator.
yield from semantics in an asynchronous generator
In an asynchronous generator, the statement
RESULT = yield from EXPR
is roughly equivalent to the following:
aiterator = aiter(EXPR)
try:
item = await anext(aiterator)
except StopAsyncIteration as stop:
RESULT = stop.value
else:
while True:
try:
received = yield item
except GeneratorExit as gen_exit:
try:
aclose = aiterator.aclose
except AttributeError:
pass
else:
await aclose()
raise gen_exit
except BaseException as exception:
try:
athrow = aiterator.athrow
except AttributeError:
raise exception from None
else:
try:
item = await athrow(exception)
except StopAsyncIteration as stop:
RESULT = stop.value
break
else:
try:
if received is None:
item = await anext(aiterator)
else:
item = await aiterator.asend(received)
except StopAsyncIteration as stop:
RESULT = stop.value
break
Rationale
Relation to synchronous generators
This PEP aims to be very similar to the semantics of synchronous yield from,
with the exception that asynchronous generator methods are used instead of synchronous
generator methods when delegating. This is a very intuitive design and furthers
symmetry with synchronous generators.
Choice of yield from as the syntax
This PEP uses yield from as the syntax, but it is acknowledged that
this is somewhat controversial, as this is an asynchronous context switch
without an explicit async or await marker; see
Rejected Ideas. In short, yield from was chosen
as the best choice of syntax because it is symmetric with synchronous
generators and easy to type.
Backwards Compatibility
This PEP introduces a backwards-compatible syntax change.
The addition of the value attribute to StopAsyncIteration is a
minor semantic change to an existing built-in exception, but is unlikely
to affect existing code in practice, as it mirrors the existing value
attribute on StopIteration and does not affect any other behavior
on StopAsyncIteration or the asynchronous iterator protocol.
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 naturally reach
for yield from given their own background knowledge about generators
in Python.
Reference Implementation
A reference implementation of this PEP can be found at python/cpython#145716.
Rejected Ideas
Using async yield from as the syntax
There are two primary counterarguments against this proposal as it is currently written:
- It adds behavior that does asynchronous work without having
asyncorawaitas its first non-whitespace token. - It changes the meaning of
yield fromdepending on the type of generator it is used in.
The solution to these issues was to introduce a new async yield from
statement that would perform asynchronous subgenerator delegation. However,
this new syntax came with its own set of problems.
First and foremost, async yield from is very verbose. There are no other
syntax constructs in Python that use three keywords in a row.
Second, there is no async yield as a counterpart; CPython already emits
significantly different bytecode for yield statements inside asynchronous
generators, so it’s not far-fetched for yield from to do the same.
Third, yield (as well as yield from) is, technically speaking, already
an asynchronous context switch, because the generator will be suspended and can
only be resumed via an await (on asend()) from the caller.
Finally, plain yield from is more symmetric and intuitive to users of
Python. To visualize, take these two examples:
def gen():
yield 1
async def agen():
yield 1
def gen():
yield from subgen()
async def agen():
yield from asubgen()
async yield from would break this symmetry.
However, all in all, the difference between yield from and
async yield from is primarily up to the taste of the reader. Even the
author of this PEP is not completely convinced for one way or the other.
During discussion, the difference between yield from and async yield from
was compared to deciding what 0^0 should be in mathematics; it depends on the
context and the point of view. The alternative was to not add support for
delegation to asynchronous subgenerators, which was a lose-lose scenario for
all parties involved. A decision had to be made, and it was clear that consensus
was never going to be reached.
async from, await from, and similar spellings
As an alternative solution to the verbosity of async yield from, some have
suggested using spellings such as async from in order to cut down on the
verbosity. Unfortunately, changes in the spelling will likely hurt the
readability of the syntax as a whole, and thus would not improve the argument
for async yield from.
The benefit of async yield from is that it specifies each of the three
important parts without introducing new keywords. In particular:
asyncis necessary to imply an asynchronous context switch.yieldis necessary to indicate that the generator will be suspended.fromis necessary to differentiate between “standard” generator suspension (ayieldstatement) and subgenerator delegation.
Given these three constraints, it seems unlikely that a more concise spelling exists.
Allowing delegation to synchronous subgenerators
In an earlier revision of this proposal, yield from in an asynchronous
generator would delegate to a synchronous generator (and async yield from
was the counterpart for asynchronous subgenerator delegation).
This had a number of hidden issues.
In particular, the mixing of asynchronous frames with synchronous frames had a
layer of complexity unfit for Python. In the implementation, there would have
to be a hidden translation layer between synchronous generator methods and
asynchronous generator methods: asend() to send(),
athrow() to throw(), and aclose()
to close().
It’s trivial for anyone who needs to delegate to a subgenerator to write
the wrapper class to upgrade a synchronous Iterable
or Generator to an
async one before calling async yield from.
class AsAsyncIterator:
def __init__(self, wrapped):
self._wrapped = iter(wrapped)
def __aiter__(self):
return self
async def __anext__(self):
try:
return self._wrapped.__next__()
except StopAsyncIteration as e:
raise RuntimeError("async generator raised StopAsyncIteration") from e
except StopIteration as e:
raise StopAsyncIteration(e.value) from e
class AsAsyncGenerator(AsAsyncIterator):
async def asend(self, value):
try:
return self._wrapped.send(value)
except StopAsyncIteration as e:
raise RuntimeError("async generator raised StopAsyncIteration") from e
except StopIteration as e:
raise StopAsyncIteration(e.value) from e
async def athrow(self, exc):
try:
return self._wrapped.throw(exc)
except StopAsyncIteration as e:
raise RuntimeError("async generator raised StopAsyncIteration") from e
except StopIteration as e:
raise StopAsyncIteration(e.value) from e
async def aclose(self):
try:
return self._wrapped.close()
except StopAsyncIteration as e:
raise RuntimeError("async generator raised StopAsyncIteration") from e
async def agen():
yield from AsAsyncIterator([1, 2, 3])
yield from AsAsyncGenerator(subgen())
To quote Brandt Bucher (paraphrased):
At that point, why not just allow synchronous functions to await coroutines?
In addition, there seemed to be much less demand for this feature compared to support for asynchronous delegation, so solving these issues is less of a priority for now.
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.
Special thanks to Yury Selivanov for providing extensive feedback and also collecting outside opinions about the design and implementation.
Change History
- 18-Jul-2026
- Switched from
async yield fromtoyield fromas the choice of syntax.
- Switched from
- 26-May-2026
- Removed support for delegating to a synchronous subgenerator (via
a plain
yield from).
- Removed support for delegating to a synchronous subgenerator (via
a plain
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.