Skip to content

Commit e9d8864

Browse files
PEP 828: Switch to yield from as the choice of syntax (#5047)
1 parent b5645ce commit e9d8864

1 file changed

Lines changed: 85 additions & 106 deletions

File tree

peps/pep-0828.rst

Lines changed: 85 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ Abstract
1515
========
1616

1717
This PEP introduces support for :keyword:`yield from <yield>` in an
18-
:ref:`asynchronous generator function <asynchronous-generator-functions>`
19-
through a new ``async yield from`` construct:
18+
:ref:`asynchronous generator function <asynchronous-generator-functions>`:
2019

2120
.. code-block:: python
2221
@@ -26,7 +25,7 @@ through a new ``async yield from`` construct:
2625
return 3
2726
2827
async def main():
29-
result = async yield from agenerator()
28+
result = yield from agenerator()
3029
assert result == 3
3130
3231
@@ -39,8 +38,7 @@ This is not to be confused with an :term:`asynchronous generator iterator`,
3938
which is the object *returned* by an asynchronous generator.
4039

4140
This PEP also uses the term "subgenerator" to refer to a generator, synchronous
42-
or asynchronous, that is used inside of a ``yield from`` or ``async yield from``
43-
expression.
41+
or asynchronous, that is used inside a ``yield from``.
4442

4543

4644
Motivation
@@ -104,38 +102,18 @@ item. This comes with a few drawbacks:
104102
Specification
105103
=============
106104

107-
108-
Syntax
109-
------
110-
111-
112105
Compiler changes
113-
^^^^^^^^^^^^^^^^
106+
----------------
114107

115108
The compiler will no longer emit a :exc:`SyntaxError` for
116-
:keyword:`return` statements inside asynchronous generators.
117-
118-
119-
Grammar changes
120-
^^^^^^^^^^^^^^^
121-
122-
The ``yield_expr`` and ``simple_stmt`` rules need to be updated for the new
123-
``async yield from`` syntax:
124-
125-
.. code-block:: peg
126-
127-
yield_expr[expr_ty]:
128-
| 'async' 'yield' 'from' a=expression
129-
130-
simple_stmt[stmt_ty] (memo):
131-
| &('yield' | 'async') yield_stmt
109+
:keyword:`return` or ``yield from`` statements inside asynchronous generators.
132110

133111

134112
Changes to ``StopAsyncIteration``
135113
---------------------------------
136114

137115
The :class:`StopAsyncIteration` exception will gain a new ``value`` attribute
138-
to be used as the result of ``async yield from`` expressions.
116+
to be used as the result of ``yield from`` expressions in asynchronous generators.
139117

140118
This attribute can be supplied by passing a positional argument to
141119
``StopAsyncIteration``. For example:
@@ -156,18 +134,18 @@ If no argument is supplied, ``value`` will be ``None``.
156134
In the body of an asynchronous generator function, the statement
157135
``return expression`` is roughly equivalent to
158136
``raise StopAsyncIteration(expression)``. However, similar to implicit
159-
``StopIteration`` exceptions raised inside of synchronous generators,
137+
``StopIteration`` exceptions raised inside synchronous generators,
160138
the exception cannot be caught in the body of the asynchronous generator.
161139

162140

163-
``async yield from`` semantics
164-
------------------------------
141+
``yield from`` semantics in an asynchronous generator
142+
-----------------------------------------------------
165143

166-
The statement
144+
In an asynchronous generator, the statement
167145

168146
.. code-block:: python
169147
170-
RESULT = async yield from EXPR
148+
RESULT = yield from EXPR
171149
172150
is roughly equivalent to the following:
173151

@@ -216,26 +194,24 @@ Rationale
216194
=========
217195

218196

219-
Relation to ``yield from``
220-
--------------------------
197+
Relation to synchronous generators
198+
----------------------------------
221199

222-
This PEP aims to be very similar to the semantics of ``yield from``, with the
223-
exception that asynchronous generator methods are used instead of synchronous
200+
This PEP aims to be very similar to the semantics of synchronous ``yield from``,
201+
with the exception that asynchronous generator methods are used instead of synchronous
224202
generator methods when delegating. This is a very intuitive design and furthers
225203
symmetry with synchronous generators.
226204

227205

228-
Choice of ``async yield from`` as the syntax
229-
--------------------------------------------
206+
Choice of ``yield from`` as the syntax
207+
--------------------------------------
230208

231-
This PEP uses ``async yield from`` as the syntax to ensure that the behavior
232-
of the syntax is immediately clear to the user.
233-
234-
However, it is acknowledged that this is somewhat verbose. There is not any
235-
great solution to this problem; see :ref:`pep-828-rejected-ideas` for
236-
discussion about proposed alternatives. In short, ``async yield from`` was
237-
chosen as the best choice of syntax because, while verbose, it is very clear
238-
and readable.
209+
This PEP uses ``yield from`` as the syntax, but it is acknowledged that
210+
this is somewhat controversial, as this is an asynchronous context switch
211+
without an explicit ``async`` or ``await`` marker; see
212+
:ref:`pep-828-rejected-ideas`. In short, ``yield from`` was chosen
213+
as the best choice of syntax because it is symmetric with synchronous
214+
generators and easy to type.
239215

240216

241217
Backwards Compatibility
@@ -244,7 +220,7 @@ Backwards Compatibility
244220
This PEP introduces a backwards-compatible syntax change.
245221

246222
The addition of the ``value`` attribute to :exc:`StopAsyncIteration` is a
247-
minor semantic change to an existing builtin exception, but is unlikely
223+
minor semantic change to an existing built-in exception, but is unlikely
248224
to affect existing code in practice, as it mirrors the existing ``value``
249225
attribute on :exc:`StopIteration` and does not affect any other behavior
250226
on ``StopAsyncIteration`` or the asynchronous iterator protocol.
@@ -262,10 +238,8 @@ How to Teach This
262238
The details of this proposal will be located in Python's canonical
263239
documentation, as with all other language constructs. However, this PEP
264240
intends to be very intuitive; users should be able to naturally reach
265-
for ``async yield from`` given their own background knowledge about generators
266-
in Python. This can be encouraged further by suggesting
267-
``async yield from`` in the error message when a user attempts to use
268-
``yield from`` in an asynchronous generator.
241+
for ``yield from`` given their own background knowledge about generators
242+
in Python.
269243

270244

271245
Reference Implementation
@@ -281,74 +255,74 @@ Rejected Ideas
281255
==============
282256

283257

284-
Using ``yield from`` to delegate to asynchronous generators
285-
-----------------------------------------------------------
258+
Using ``async yield from`` as the syntax
259+
----------------------------------------
286260

287-
Due to the verbosity of ``async yield from``, it was proposed to overload
288-
the existing ``yield from`` syntax to perform asynchronous subgenerator
289-
delegation when used inside of an asynchronous generator.
261+
There are two primary counterarguments against this proposal as it is currently
262+
written:
290263

291-
For example:
264+
1. It adds behavior that does asynchronous work without having ``async`` or
265+
``await`` as its first non-whitespace token.
266+
2. It changes the meaning of ``yield from`` depending on the type of generator
267+
it is used in.
292268

293-
.. code-block:: python
269+
The solution to these issues was to introduce a new ``async yield from``
270+
statement that would perform asynchronous subgenerator delegation. However,
271+
this new syntax came with its own set of problems.
294272

295-
async def asubgenerator():
296-
yield 1
297-
yield 2
273+
First and foremost, ``async yield from`` is very verbose. There are no other
274+
syntax constructs in Python that use three keywords in a row.
298275

299-
async def agenerator():
300-
yield from asubgenerator()
276+
Second, there is no ``async yield`` as a counterpart; CPython already emits
277+
significantly different bytecode for ``yield`` statements inside asynchronous
278+
generators, so it's not far-fetched for ``yield from`` to do the same.
301279

280+
Third, ``yield`` (as well as ``yield from``) is, technically speaking, already
281+
an asynchronous context switch, because the generator will be suspended and can
282+
only be resumed via an ``await`` (on :meth:`~agen.asend`) from the caller.
302283

303-
This has the benefit of being more concise than ``async yield from``, but also
304-
has a few downsides.
284+
Finally, plain ``yield from`` is more symmetric and intuitive to users of
285+
Python. To visualize, take these two examples:
305286

306-
Most importantly, this makes the asynchronous context switches necessary for
307-
delegation implicit, which has no precedent in Python; all syntax that may
308-
execute an ``await`` is prefixed with ``async``. It has been argued that one
309-
of the upsides of ``async``/``await`` over threads is the explicit switch
310-
points, so hiding ``await``\ s behind a ``yield from`` hurts this benefit.
287+
.. code-block:: python
311288
312-
Second, many were uncomfortable with ``yield from`` being context-dependent.
313-
It felt like a potential footgun for ``yield from`` to mean something
314-
different based on the type of generator it was used in. In practice, this
315-
may come up in a scenario where one wants to convert a synchronous generator
316-
into an asynchronous generator.
289+
def gen():
290+
yield 1
317291
318-
For example, imagine a developer is writing a function for streaming data
319-
to the caller:
292+
async def agen():
293+
yield 1
320294
321295
.. code-block:: python
322296
323-
def stream_data():
324-
yield ...
325-
yield from something_else()
326-
yield ...
297+
def gen():
298+
yield from subgen()
327299
328-
Now, imagine that the developer wants to add an ``await`` call somewhere in
329-
this function; the ``yield from something_else()`` statement would suddenly
330-
become a runtime :class:`TypeError` (as opposed to a compile-time
331-
:class:`SyntaxError`). With the current proposal, the existence of
332-
``async yield from`` (which would ideally be included in the error message)
333-
would make it much clearer that ``something_else`` must also be asynchronous
334-
in order to delegate to it.
300+
async def agen():
301+
yield from asubgen()
302+
303+
304+
``async yield from`` would break this symmetry.
335305

336-
Finally, this would preclude the introduction of support for synchronous
337-
subgenerator delegation inside asynchronous generators (see
338-
:ref:`pep-828-synchronous-delegation`), because the ``yield from``
339-
syntax would already be overloaded. However, the author of this proposal
340-
does acknowledge that the issues with synchronous subdelegation may preclude
341-
the introduction of this anyway -- it is not entirely clear whether the issues
342-
are solvable given time.
306+
However, all in all, the difference between ``yield from`` and
307+
``async yield from`` is primarily up to the taste of the reader. Even the
308+
author of this PEP is not completely convinced for one way or the other.
309+
During discussion, the difference between ``yield from`` and ``async yield from``
310+
was compared to `deciding what 0^0 should be in mathematics
311+
<https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero>`_; it depends on the
312+
context and the point of view. The alternative was to not add support for
313+
delegation to asynchronous subgenerators, which was a lose-lose scenario for
314+
all parties involved. A decision had to be made, and it was clear that consensus
315+
was never going to be reached.
343316

344317

345318
``async from``, ``await from``, and similar spellings
346-
-----------------------------------------------------
319+
*****************************************************
347320

348-
As an alternate solution to the verbosity of ``async yield from``, some have
321+
As an alternative solution to the verbosity of ``async yield from``, some have
349322
suggested using spellings such as ``async from`` in order to cut down on the
350323
verbosity. Unfortunately, changes in the spelling will likely hurt the
351-
readability of the syntax as a whole.
324+
readability of the syntax as a whole, and thus would not improve the argument
325+
for ``async yield from``.
352326

353327
The benefit of ``async yield from`` is that it specifies each of the three
354328
important parts without introducing new keywords. In particular:
@@ -367,10 +341,10 @@ exists.
367341
Allowing delegation to synchronous subgenerators
368342
------------------------------------------------
369343

370-
In an earlier revision of this proposal, the synchronous ``yield from``
371-
construct was allowed in an asynchronous generator, which would delegate to a
372-
synchronous generator from an asynchronous one. This had a number of hidden
373-
issues.
344+
In an earlier revision of this proposal, ``yield from`` in an asynchronous
345+
generator would delegate to a synchronous generator (and ``async yield from``
346+
was the counterpart for asynchronous subgenerator delegation).
347+
This had a number of hidden issues.
374348

375349
In particular, the mixing of asynchronous frames with synchronous frames had a
376350
layer of complexity unfit for Python. In the implementation, there would have
@@ -379,7 +353,7 @@ asynchronous generator methods: :meth:`~agen.asend` to :meth:`~generator.send`,
379353
:meth:`~agen.athrow` to :meth:`~generator.throw`, and :meth:`~agen.aclose`
380354
to :meth:`~generator.close`.
381355

382-
It's trivial for anyone that needs to delegate to a subgenerator to write
356+
It's trivial for anyone who needs to delegate to a subgenerator to write
383357
the wrapper class to upgrade a synchronous :class:`~collections.abc.Iterable`
384358
or :class:`~collections.abc.Generator` to an
385359
async one before calling ``async yield from``.
@@ -427,8 +401,8 @@ async one before calling ``async yield from``.
427401
428402
429403
async def agen():
430-
async yield from AsAsyncIterator([1, 2, 3])
431-
async yield from AsAsyncGenerator(subgen())
404+
yield from AsAsyncIterator([1, 2, 3])
405+
yield from AsAsyncGenerator(subgen())
432406
433407
434408
To quote Brandt Bucher (paraphrased):
@@ -457,6 +431,11 @@ collecting outside opinions about the design and implementation.
457431
Change History
458432
==============
459433

434+
- 18-Jul-2026
435+
436+
- Switched from ``async yield from`` to ``yield from`` as the choice of
437+
syntax.
438+
460439
- 26-May-2026
461440

462441
- Removed support for delegating to a synchronous subgenerator (via

0 commit comments

Comments
 (0)