Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 828 – Supporting ‘yield from’ in asynchronous generators

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

Table of Contents

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 implement yield from support 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:

  1. https://discuss.python.org/t/8897
  2. https://discuss.python.org/t/47050
  3. 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:

  1. 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.
  2. asend(), athrow(), and aclose() do not interact properly with the caller. This is the primary reason that yield from was added in the first place.
  3. 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:

  1. It adds behavior that does asynchronous work without having async or await as its first non-whitespace token.
  2. It changes the meaning of yield from depending 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:

  1. async is necessary to imply an asynchronous context switch.
  2. yield is necessary to indicate that the generator will be suspended.
  3. from is necessary to differentiate between “standard” generator suspension (a yield statement) 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 from to yield from as the choice of syntax.
  • 26-May-2026
    • Removed support for delegating to a synchronous subgenerator (via a plain yield from).