From 8fb10ee858332ef4ee483fde67b1478d391f3c5a Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Mon, 24 Nov 2025 02:15:13 +0500 Subject: [PATCH 1/6] Add async generators best practices section --- Doc/library/asyncio-dev.rst | 102 ++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index 7831b613bd4a60..f4e7721a27b34e 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -248,3 +248,105 @@ Output in debug mode:: File "../t.py", line 4, in bug raise Exception("not consumed") Exception: not consumed + +Asynchronous generators best practices +====================================== + +By :term:`asynchronous generator` in this section we will mean +an :term:`asynchronous generator iterator` that returned by +:term:`asynchronous generator` function. + +Manually close the generator +---------------------------- + +If an asynchronous generator happens to exit early by :keyword:`break`, the caller +task being cancelled, or other exceptions, the generator's async cleanup code +will run and possibly raise exceptions or access context variables in an +unexpected context--perhaps after the lifetime of tasks it depends, or +during the event loop shutdown when the async-generator garbage collection hook +is called. + +To prevent this, it is recommended to explicitly close the async generator by +calling :meth:`~agen.aclose` method, or using :func:`contextlib.aclosing` context +manager:: + + import asyncio + import contextlib + + async def gen(): + yield 1 + yield 2 + + async def func(): + async with contextlib.aclosing(gen()) as g: + async for x in g: + break + + asyncio.run(func()) + + +Only create a generator when a loop is already running +------------------------------------------------------ + +As said abovew, if an asynchronous generator is not resumed before it is +finalized, then any finalization procedures will be delayed. The event loop +handles this situation and doing it best to call async generator-iterator's +:meth:`~agen.aclose` at the proper moment, thus allowing any pending +:keyword:`!finally` clauses to execute. + +Then it is recomended to create async generators only after the event loop +has already been created. + + +Avoid iterating and closing the same generator concurrently +----------------------------------------------------------- + +The async generators allow to be reentered while another +:meth:`~agen.aclose`/:meth:`~agen.aclose`/:meth:`~agen.aclose` call is in +progress. This may lead to an inconsistent state of the async generator +and cause errors. + +Let's consider following example:: + + import asyncio + + async def consumer(): + for idx in range(100): + await asyncio.sleep(0) + message = yield idx + print('received', message) + + async def amain(): + agenerator = consumer() + await agenerator.asend(None) + + fa = asyncio.create_task(agenerator.asend('A')) + fb = asyncio.create_task(agenerator.asend('B')) + await fa + await fb + + asyncio.run(amain()) + +Output:: + + received A + Traceback (most recent call last): + File "test.py", line 38, in + asyncio.run(amain()) + ~~~~~~~~~~~^^^^^^^^^ + File "Lib\asyncio\runners.py", line 204, in run + return runner.run(main) + ~~~~~~~~~~^^^^^^ + File "Lib\asyncio\runners.py", line 127, in run + return self._loop.run_until_complete(task) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ + File "Lib\asyncio\base_events.py", line 719, in run_until_complete + return future.result() + ~~~~~~~~~~~~~^^ + File "test.py", line 36, in amain + await fb + RuntimeError: anext(): asynchronous generator is already running + + +Therefore, it is recommended to avoid using the async generators in parallel +tasks or in the multiple event loops. From ae50f3f91d272de8998b07d2cc8dea0549dbee91 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Mon, 24 Nov 2025 10:32:22 +0500 Subject: [PATCH 2/6] Update Doc/library/asyncio-dev.rst Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Doc/library/asyncio-dev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index f4e7721a27b34e..cb3d980747f5d2 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -288,7 +288,7 @@ manager:: Only create a generator when a loop is already running ------------------------------------------------------ -As said abovew, if an asynchronous generator is not resumed before it is +As said above, if an asynchronous generator is not resumed before it is finalized, then any finalization procedures will be delayed. The event loop handles this situation and doing it best to call async generator-iterator's :meth:`~agen.aclose` at the proper moment, thus allowing any pending From 99f148c3f77e0c6ac9b3418cec3b8732b3548c7a Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Mon, 24 Nov 2025 10:36:09 +0500 Subject: [PATCH 3/6] Fix spacing between sections --- Doc/library/asyncio-dev.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index cb3d980747f5d2..129be11895e8ab 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -249,6 +249,7 @@ Output in debug mode:: raise Exception("not consumed") Exception: not consumed + Asynchronous generators best practices ====================================== @@ -284,7 +285,6 @@ manager:: asyncio.run(func()) - Only create a generator when a loop is already running ------------------------------------------------------ @@ -297,7 +297,6 @@ handles this situation and doing it best to call async generator-iterator's Then it is recomended to create async generators only after the event loop has already been created. - Avoid iterating and closing the same generator concurrently ----------------------------------------------------------- From 11242b452d9da59a728e0711aaa13d7cb35e7045 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 27 Nov 2025 00:46:27 +0500 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Guido van Rossum --- Doc/library/asyncio-dev.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index 129be11895e8ab..a029a7802d87f7 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -254,7 +254,7 @@ Asynchronous generators best practices ====================================== By :term:`asynchronous generator` in this section we will mean -an :term:`asynchronous generator iterator` that returned by +an :term:`asynchronous generator iterator` that is returned by an :term:`asynchronous generator` function. Manually close the generator @@ -262,13 +262,12 @@ Manually close the generator If an asynchronous generator happens to exit early by :keyword:`break`, the caller task being cancelled, or other exceptions, the generator's async cleanup code -will run and possibly raise exceptions or access context variables in an -unexpected context--perhaps after the lifetime of tasks it depends, or +will run in an unexpected context -- perhaps after the lifetime of tasks it depends on, or during the event loop shutdown when the async-generator garbage collection hook is called. To prevent this, it is recommended to explicitly close the async generator by -calling :meth:`~agen.aclose` method, or using :func:`contextlib.aclosing` context +calling the :meth:`~agen.aclose` method, or using a :func:`contextlib.aclosing` context manager:: import asyncio @@ -281,7 +280,7 @@ manager:: async def func(): async with contextlib.aclosing(gen()) as g: async for x in g: - break + break # Don't iterate until the end asyncio.run(func()) @@ -290,9 +289,9 @@ Only create a generator when a loop is already running As said above, if an asynchronous generator is not resumed before it is finalized, then any finalization procedures will be delayed. The event loop -handles this situation and doing it best to call async generator-iterator's -:meth:`~agen.aclose` at the proper moment, thus allowing any pending -:keyword:`!finally` clauses to execute. +handles this situation and doing its best to call the async generator-iterator's +:meth:`~agen.aclose` method at the proper moment, thus allowing any pending +:keyword:`!finally` clauses to run. Then it is recomended to create async generators only after the event loop has already been created. @@ -300,10 +299,10 @@ has already been created. Avoid iterating and closing the same generator concurrently ----------------------------------------------------------- -The async generators allow to be reentered while another +Async generators may to be reentered while another :meth:`~agen.aclose`/:meth:`~agen.aclose`/:meth:`~agen.aclose` call is in progress. This may lead to an inconsistent state of the async generator -and cause errors. +and can cause errors. Let's consider following example:: @@ -347,5 +346,5 @@ Output:: RuntimeError: anext(): asynchronous generator is already running -Therefore, it is recommended to avoid using the async generators in parallel -tasks or in the multiple event loops. +Therefore, it is recommended to avoid using async generators in parallel +tasks or from multiple event loops. From 3556aefd9e7732aa15285f9e7216ccca75a01c9e Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 27 Nov 2025 00:54:56 +0500 Subject: [PATCH 5/6] Fix copy-paste typo --- Doc/library/asyncio-dev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index a029a7802d87f7..f4bf34202064c4 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -300,7 +300,7 @@ Avoid iterating and closing the same generator concurrently ----------------------------------------------------------- Async generators may to be reentered while another -:meth:`~agen.aclose`/:meth:`~agen.aclose`/:meth:`~agen.aclose` call is in +:meth:`~agen.anext`/:meth:`~agen.athrow`/:meth:`~agen.aclose` call is in progress. This may lead to an inconsistent state of the async generator and can cause errors. From a70038a976e364adf7dd8ae966c5d9ab7b536f83 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Thu, 27 Nov 2025 02:41:38 +0500 Subject: [PATCH 6/6] Update section about iterating agen after running event loop --- Doc/library/asyncio-dev.rst | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Doc/library/asyncio-dev.rst b/Doc/library/asyncio-dev.rst index f4bf34202064c4..20543a52bfcd0d 100644 --- a/Doc/library/asyncio-dev.rst +++ b/Doc/library/asyncio-dev.rst @@ -257,6 +257,7 @@ By :term:`asynchronous generator` in this section we will mean an :term:`asynchronous generator iterator` that is returned by an :term:`asynchronous generator` function. + Manually close the generator ---------------------------- @@ -284,18 +285,32 @@ manager:: asyncio.run(func()) + Only create a generator when a loop is already running ------------------------------------------------------ -As said above, if an asynchronous generator is not resumed before it is -finalized, then any finalization procedures will be delayed. The event loop -handles this situation and doing its best to call the async generator-iterator's -:meth:`~agen.aclose` method at the proper moment, thus allowing any pending -:keyword:`!finally` clauses to run. - -Then it is recomended to create async generators only after the event loop +It is recommended to create asynchronous generators only after the event loop has already been created. +To ensure that asynchronous generators close reliably, the event loop uses the +:func:`sys.set_asyncgen_hooks` function to register callback functions. These +callbacks update the list of running asynchronous generators to keep it in a +consistent state. + +When the :meth:`loop.shutdown_asyncgens() ` +function is called, the running generators are stopped gracefully, and the +list is cleared. + +The asynchronous generator calls the corresponding system hook when on the +first iteration. At the same time, the generator remembers that the hook was +called and don't call it twice. + +So, if the iteration begins before the event loop is created, the event loop +will not be able to add it to its list of active generators because the hooks +will be set after the generator tries to call it. Consequently, the event loop +will not be able to terminate the generator if necessary. + + Avoid iterating and closing the same generator concurrently -----------------------------------------------------------