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