Skip to content
11 changes: 11 additions & 0 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,19 @@ The following exceptions are the exceptions that are usually raised.
Must be raised by :meth:`~object.__anext__` method of an
:term:`asynchronous iterator` object to stop the iteration.

.. attribute:: StopAsyncIteration.value

This is given as an argument when constructing the exception, and
defaults to :const:`None`. This is used for the result of
``yield from`` expressions (see :ref:`async-yield-from`).

.. versionadded:: next

.. versionadded:: 3.5

.. versionchanged:: next
Added the ``value`` attribute.

.. exception:: SyntaxError(message, details)

Raised when the parser encounters a syntax error. This may occur in an
Expand Down
91 changes: 91 additions & 0 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,10 @@ the yield expression. It can be either set explicitly when raising
.. versionchanged:: 3.3
Added ``yield from <expr>`` to delegate control flow to a subiterator.

.. versionchanged:: next
``yield from`` is now allowed to be used in an async generator.
Previously, it would raise a :class:`SyntaxError`.

The parentheses may be omitted when the yield expression is the sole expression
on the right hand side of an assignment statement.

Expand All @@ -1179,6 +1183,10 @@ on the right hand side of an assignment statement.
The proposal that expanded on :pep:`492` by adding generator capabilities to
coroutine functions.

:pep:`828` - Supporting ``yield from`` in asynchronous generators
The proposal that expanded on :pep:`380` by adding subgenerator delegation
to asynchronous generators.

.. index:: pair: object; generator
.. _generator-methods:

Expand Down Expand Up @@ -1367,6 +1375,89 @@ of a *finalizer* method see the implementation of
The expression ``yield from <expr>`` is a syntax error when used in an
asynchronous generator function.

.. _async-yield-from:

Asynchronous ``yield from``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

In async generators, the ``yield from`` statement operates solely on
asynchronous constructs rather than synchronous ones.
In particular:

.. list-table::
:widths: auto
:header-rows: 1

* * Synchronous ``yield from``
* Asynchronous ``yield from``
* * :meth:`~object.__iter__`
* :meth:`~object.__aiter__`
* * :meth:`~generator.__next__`
* :meth:`~agen.__anext__`
* * :meth:`~generator.send`
* :meth:`~agen.asend`
* * :class:`StopIteration`
* :class:`StopAsyncIteration`

To describe the above:

* The object being delegated to must be asynchronously iterable (that is, it
must implement ``__aiter__`` instead of ``__iter__``).
* When ``anext`` is called on the parent generator (the one that contains
``yield from``), ``__anext__`` will be invoked on the subgenerator.
In contrast, a synchronous ``yield from`` would invoke ``__next__`` instead.
(Note that calling ``asend`` with a ``None`` value is equivalent to calling
``anext()``, and thus applies here.)
* All calls to ``asend``, ``athrow``, and ``aclose`` are delegated to the
subgenerator (the object returned by ``__aiter__`` in this case). This means
that a call to ``parent_generator.asend(x)`` is semantically equivalent to
``sub_generator.asend(x)``, where ``parent_generator`` is currently executing
an asynchronous ``yield from`` on ``sub_generator``.
* The result of the expression is retrieved through
:attr:`StopAsyncIteration.value` instead of :attr:`StopIteration.value`.

An example of usage for ``yield from`` in async generator:

.. code-block:: pycon

>>> import asyncio
>>> async def sleepy_count(number):
... for num in range(number):
... await asyncio.sleep(1)
... result = yield num
... print(f"Got result: {result}")
...
>>> async def counter():
... final_number = yield from sleepy_count(5)
... yield final_number
...
>>> ag = counter()
>>> await anext(ag)
Comment thread
ZeroIntensity marked this conversation as resolved.
0
>>> await anext(ag)
Got result: None
1
>>> await ag.asend(42)
Got result: 42
2
>>> await ag.athrow(ValueError("Nobody expects the Spanish Inquisition"))
Traceback (most recent call last):
File "/home/python/cpython/Lib/concurrent/futures/_base.py", line 450, in result
return self.__get_result()
~~~~~~~~~~~~~~~~~^^
File "/home/python/cpython/Lib/concurrent/futures/_base.py", line 395, in __get_result
raise self._exception
File "<python-input-4>", line 1, in <module>
await ag.athrow(ValueError("Nobody expects the Spanish Inquisition"))
File "<python-input-0>", line 8, in counter
final_number = yield from sleepy_count(4)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<python-input-0>", line 3, in sleepy_count
result = yield num
^^^^^^^^^
ValueError: Nobody expects the Spanish Inquisition


.. index:: pair: object; asynchronous-generator
.. _asynchronous-generator-methods:

Expand Down
27 changes: 26 additions & 1 deletion Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,38 @@ Summary --- release highlights
Brevity is key.


.. PEP-sized items next.
* :pep:`828`: :ref:`'yield from' in async generators <whatsnew315-async-yield-from>`



New features
============

.. _whatsnew315-async-yield-from:

:pep:`828`: Supporting ``yield from`` in asynchronous generators
----------------------------------------------------------------

Use of the :keyword:`yield from <yield>` construct and the :keyword:`return`
statement with a non-``None`` value is now allowed in an
:term:`asynchronous generator function <asynchronous generator>`. For example,
the following code would previously raise a :class:`SyntaxError`:

.. code-block:: python

async def asubgen():
yield 2
yield 3
yield 4

async def agenerator():
yield 1
yield from asubgen() # Now allowed!
return 5 # Now allowed!

.. seealso:: :pep:`828` for further details.

(Contributed by Peter Bierma in :gh:`155126`.)


Other language changes
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ typedef struct {
PyObject *value;
} PyStopIterationObject;

typedef struct {
PyException_HEAD
PyObject *value;
} PyStopAsyncIterationObject;

typedef struct {
PyException_HEAD
PyObject *name;
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_magic_number.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ Known values:
Python 3.16a1 3703 (Replace DELETE_GLOBAL with PUSH_NULL; STORE_GLOBAL)
Python 3.16a1 3704 (Replace DELETE_ATTR with PUSH_NULL; STORE_ATTR)
Python 3.16a1 3705 (Add INTRINSIC_ADD_CONDITIONAL_ANNOTATION)
Python 3.16a1 3706 (PEP 828: yield from for asyncgens)

Python 3.17 will start with 3750

Expand All @@ -312,7 +313,7 @@ Known values:

*/

#define PYC_MAGIC_NUMBER 3705
#define PYC_MAGIC_NUMBER 3706
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
(little-endian) and then appending b'\r\n'. */
#define PYC_MAGIC_NUMBER_TOKEN \
Expand Down
9 changes: 7 additions & 2 deletions Include/internal/pycore_opcode_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Include/internal/pycore_uop_ids.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading