PEP: 828
Title: Supporting 'yield from' in asynchronous generators
Author: Peter Bierma <peter@python.org>
PEP-Delegate: Yury Selivanov <yury@vercel.com>
Discussions-To: https://discuss.python.org/t/106459
Status: Accepted
Type: Standards Track
Created: 07-Mar-2026
Python-Version: 3.16
Post-History: `07-Mar-2026 <https://discuss.python.org/t/106430>`__,
              `09-Mar-2026 <https://discuss.python.org/t/106459>`__
Resolution: `03-Aug-2026 <https://discuss.python.org/t/106459/74>`__


Abstract
========

This PEP introduces support for :keyword:`yield from <yield>` in an
:ref:`asynchronous generator function <asynchronous-generator-functions>`:

.. code-block:: python

    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 :term:`asynchronous generator`, sometimes suffixed with "function".
This is not to be confused with an :term:`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 <https://stackoverflow.com/questions/47376408>`__
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. :meth:`~agen.asend`, :meth:`~agen.athrow`, and :meth:`~agen.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 :exc:`SyntaxError` for
:keyword:`return` or ``yield from`` statements inside asynchronous generators.


Changes to ``StopAsyncIteration``
---------------------------------

The :class:`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:

.. code-block:: pycon

   >>> 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

.. code-block:: python

   RESULT = yield from EXPR

is roughly equivalent to the following:

.. code-block:: python

    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
:ref:`pep-828-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 :exc:`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 :exc:`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 <https://github.com/python/cpython/pull/145716>`__.


.. _pep-828-rejected-ideas:

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 :meth:`~agen.asend`) from the caller.

Finally, plain ``yield from`` is more symmetric and intuitive to users of
Python. To visualize, take these two examples:

.. code-block:: python

    def gen():
        yield 1

    async def agen():
        yield 1

.. code-block:: python

    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
<https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero>`_; 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.


.. _pep-828-synchronous-delegation:

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: :meth:`~agen.asend` to :meth:`~generator.send`,
:meth:`~agen.athrow` to :meth:`~generator.throw`, and :meth:`~agen.aclose`
to :meth:`~generator.close`.

It's trivial for anyone who needs to delegate to a subgenerator to write
the wrapper class to upgrade a synchronous :class:`~collections.abc.Iterable`
or :class:`~collections.abc.Generator` to an
async one before calling ``async yield from``.

.. code-block:: python

    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 :exc:`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 <https://github.com/python/cpython/pull/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``).


Copyright
=========

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.
