PEP 492 introduced support for native coroutines andasync/awaitsyntax to Python 3.5. It is proposed here to extend Python’sasynchronous capabilities by adding support forasynchronous generators.
Regular generators (introduced inPEP 255) enabled an elegant way ofwriting complexdata producers and have them behave like an iterator.
However, currently there is no equivalent concept for theasynchronousiteration protocol (asyncfor). This makes writing asynchronousdata producers unnecessarily complex, as one must define a class thatimplements__aiter__ and__anext__ to be able to use it inanasyncfor statement.
Essentially, the goals and rationale forPEP 255, applied to theasynchronous execution case, hold true for this proposal as well.
Performance is an additional point for this proposal: in our testing ofthe reference implementation, asynchronous generators are2x fasterthan an equivalent implemented as an asynchronous iterator.
As an illustration of the code quality improvement, consider thefollowing class that prints numbers with a given delay once iterated:
classTicker:"""Yield numbers from 0 to `to` every `delay` seconds."""def__init__(self,delay,to):self.delay=delayself.i=0self.to=todef__aiter__(self):returnselfasyncdef__anext__(self):i=self.iifi>=self.to:raiseStopAsyncIterationself.i+=1ifi:awaitasyncio.sleep(self.delay)returni
The same can be implemented as a much simpler asynchronous generator:
asyncdefticker(delay,to):"""Yield numbers from 0 to `to` every `delay` seconds."""foriinrange(to):yieldiawaitasyncio.sleep(delay)
This proposal introduces the concept ofasynchronous generators toPython.
This specification presumes knowledge of the implementation ofgenerators and coroutines in Python (PEP 342,PEP 380 andPEP 492).
A Pythongenerator is any function containing one or moreyieldexpressions:
deffunc():# a functionreturndefgenfunc():# a generator functionyield
We propose to use the same approach to defineasynchronous generators:
asyncdefcoro():# a coroutine functionawaitsmth()asyncdefasyncgen():# an asynchronous generator functionawaitsmth()yield42
The result of calling anasynchronous generator function isanasynchronous generator object, which implements the asynchronousiteration protocol defined inPEP 492.
It is aSyntaxError to have a non-emptyreturn statement in anasynchronous generator.
The protocol requires two special methods to be implemented:
__aiter__ method returning anasynchronous iterator.__anext__ method returning anawaitable object, which usesStopIteration exception to “yield” values, andStopAsyncIteration exception to signal the end of the iteration.Asynchronous generators define both of these methods. Let’s manuallyiterate over a simple asynchronous generator:
asyncdefgenfunc():yield1yield2gen=genfunc()assertgen.__aiter__()isgenassertawaitgen.__anext__()==1assertawaitgen.__anext__()==2awaitgen.__anext__()# This line will raise StopAsyncIteration.
PEP 492 requires an event loop or a scheduler to run coroutines.Because asynchronous generators are meant to be used from coroutines,they also require an event loop to run and finalize them.
Asynchronous generators can havetry..finally blocks, as well asasyncwith. It is important to provide a guarantee that, evenwhen partially iterated, and then garbage collected, generators canbe safely finalized. For example:
asyncdefsquare_series(con,to):asyncwithcon.transaction():cursor=con.cursor('SELECT generate_series(0, $1) AS i',to)asyncforrowincursor:yieldrow['i']**2asyncforiinsquare_series(con,1000):ifi==100:break
The above code defines an asynchronous generator that usesasyncwith to iterate over a database cursor in a transaction.The generator is then iterated over withasyncfor, which interruptsthe iteration at some point.
Thesquare_series() generator will then be garbage collected,and without a mechanism to asynchronously close the generator, Pythoninterpreter would not be able to do anything.
To solve this problem we propose to do the following:
aclose method on asynchronous generatorsreturning a specialawaitable. When awaited itthrows aGeneratorExit into the suspended generator anditerates over it until either aGeneratorExit oraStopAsyncIteration occur.This is very similar to what theclose() method does to regularPython generators, except that an event loop is required to executeaclose().
RuntimeError, when an asynchronous generator executesayield expression in itsfinally block (usingawaitis fine, though):asyncdefgen():try:yieldfinally:awaitasyncio.sleep(1)# Can use 'await'.yield# Cannot use 'yield',# this line will trigger a# RuntimeError.
sys module:set_asyncgen_hooks() andget_asyncgen_hooks().The idea behindsys.set_asyncgen_hooks() is to allow eventloops to intercept asynchronous generators iteration and finalization,so that the end user does not need to care about the finalizationproblem, and everything just works.
sys.set_asyncgen_hooks() accepts two arguments:
firstiter: a callable which will be called when an asynchronousgenerator is iterated for the first time.finalizer: a callable which will be called when an asynchronousgenerator is about to be GCed.When an asynchronous generator is iterated for the first time,it stores a reference to the currentfinalizer.
When an asynchronous generator is about to be garbage collected,it calls its cachedfinalizer. The assumption is that the finalizerwill schedule anaclose() call with the loop that was activewhen the iteration started.
For instance, here is how asyncio is modified to allow safefinalization of asynchronous generators:
# asyncio/base_events.pyclassBaseEventLoop:defrun_forever(self):...old_hooks=sys.get_asyncgen_hooks()sys.set_asyncgen_hooks(finalizer=self._finalize_asyncgen)try:...finally:sys.set_asyncgen_hooks(*old_hooks)...def_finalize_asyncgen(self,gen):self.create_task(gen.aclose())
The second argument,firstiter, allows event loops to maintaina weak set of asynchronous generators instantiated under their control.This makes it possible to implement “shutdown” mechanisms to safelyfinalize all open generators and close the event loop.
sys.set_asyncgen_hooks() is thread-specific, so several eventloops running in parallel threads can use it safely.
sys.get_asyncgen_hooks() returns a namedtuple-like structurewithfirstiter andfinalizer fields.
The asyncio event loop will usesys.set_asyncgen_hooks() API tomaintain a weak set of all scheduled asynchronous generators, and toschedule theiraclose() coroutine methods when it is time forgenerators to be GCed.
To make sure that asyncio programs can finalize all scheduledasynchronous generators reliably, we propose to add a new event loopcoroutine methodloop.shutdown_asyncgens(). The method willschedule all currently open asynchronous generators to close with anaclose() call.
After calling theloop.shutdown_asyncgens() method, the event loopwill issue a warning whenever a new asynchronous generator is iteratedfor the first time. The idea is that after requesting all asynchronousgenerators to be shutdown, the program should not execute code thatiterates over new asynchronous generators.
An example of howshutdown_asyncgens coroutine should be used:
try:loop.run_forever()finally:loop.run_until_complete(loop.shutdown_asyncgens())loop.close()
The object is modeled after the standard Python generator object.Essentially, the behaviour of asynchronous generators is designedto replicate the behaviour of synchronous generators, with the onlydifference in that the API is asynchronous.
The following methods and properties are defined:
agen.__aiter__(): Returnsagen.agen.__anext__(): Returns anawaitable, that performs oneasynchronous generator iteration when awaited.agen.asend(val): Returns anawaitable, that pushes theval object in theagen generator. When theagen hasnot yet been iterated,val must beNone.Example:
asyncdefgen():awaitasyncio.sleep(0.1)v=yield42print(v)awaitasyncio.sleep(0.2)g=gen()awaitg.asend(None)# Will return 42 after sleeping# for 0.1 seconds.awaitg.asend('hello')# Will print 'hello' and# raise StopAsyncIteration# (after sleeping for 0.2 seconds.)
agen.athrow(typ,[val,[tb]]): Returns anawaitable, thatthrows an exception into theagen generator.Example:
asyncdefgen():try:awaitasyncio.sleep(0.1)yield'hello'exceptZeroDivisionError:awaitasyncio.sleep(0.2)yield'world'g=gen()v=awaitg.asend(None)print(v)# Will print 'hello' after# sleeping for 0.1 seconds.v=awaitg.athrow(ZeroDivisionError)print(v)# Will print 'world' after# sleeping 0.2 seconds.
agen.aclose(): Returns anawaitable, that throws aGeneratorExit exception into the generator. Theawaitable caneither return a yielded value, ifagen handled the exception,oragen will be closed and the exception will propagate backto the caller.agen.__name__ andagen.__qualname__: readable and writablename and qualified name attributes.agen.ag_await: The object thatagen is currentlyawaitingon, orNone. This is similar to the currently availablegi_yieldfrom for generators andcr_await for coroutines.agen.ag_frame,agen.ag_running, andagen.ag_code:defined in the same way as similar attributes of standard generators.StopIteration andStopAsyncIteration are not propagated out ofasynchronous generators, and are replaced with aRuntimeError.
Asynchronous generator object (PyAsyncGenObject) shares thestruct layout withPyGenObject. In addition to that, thereference implementation introduces three new objects:
PyAsyncGenASend: the awaitable object that implements__anext__ andasend() methods.PyAsyncGenAThrow: the awaitable object that implementsathrow() andaclose() methods._PyAsyncGenWrappedValue: every directly yielded object from anasynchronous generator is implicitly boxed into this structure. Thisis how the generator implementation can separate objects that areyielded using regular iteration protocol from objects that areyielded using asynchronous iteration protocol.PyAsyncGenASend andPyAsyncGenAThrow are awaitables (they have__await__ methods returningself) and are coroutine-like objects(implementing__iter__,__next__,send() andthrow()methods). Essentially, they control how asynchronous generators areiterated:

PyAsyncGenASend is a coroutine-like object that drives__anext__andasend() methods and implements the asynchronous iterationprotocol.
agen.asend(val) andagen.__anext__() return instances ofPyAsyncGenASend (which hold references back to the parentagen object.)
The data flow is defined as follows:
PyAsyncGenASend.send(val) is called for the first time,val is pushed to the parentagen object (using existingfacilities ofPyGenObject.)Subsequent iterations over thePyAsyncGenASend objects, pushNone toagen.
When a_PyAsyncGenWrappedValue object is yielded, itis unboxed, and aStopIteration exception is raised with theunwrapped value as an argument.
PyAsyncGenASend.throw(*exc) is called for the first time,*exc is thrown into the parentagen object.Subsequent iterations over thePyAsyncGenASend objects, pushNone toagen.
When a_PyAsyncGenWrappedValue object is yielded, itis unboxed, and aStopIteration exception is raised with theunwrapped value as an argument.
return statements in asynchronous generators raiseStopAsyncIteration exception, which is propagated throughPyAsyncGenASend.send() andPyAsyncGenASend.throw() methods.PyAsyncGenAThrow is very similar toPyAsyncGenASend. The onlydifference is thatPyAsyncGenAThrow.send(), when called first time,throws an exception into the parentagen object (instead of pushinga value into it.)
types.AsyncGeneratorType – type of asynchronous generatorobject.sys.set_asyncgen_hooks() andsys.get_asyncgen_hooks()methods to set up asynchronous generators finalizers and iterationinterceptors in event loops.inspect.isasyncgen() andinspect.isasyncgenfunction()introspection functions.loop.shutdown_asyncgens().collections.abc.AsyncGenerator abstract base class.The proposal is fully backwards compatible.
In Python 3.5 it is aSyntaxError to define anasyncdeffunction with ayield expression inside, therefore it’s safe tointroduce asynchronous generators in 3.6.
There is no performance degradation for regular generators.The following micro benchmark runs at the same speed on CPython withand without asynchronous generators:
defgen():i=0whilei<100000000:yieldii+=1list(gen())
The following micro-benchmark shows that asynchronous generatorsare about2.3x faster than asynchronous iterators implemented inpure Python:
N=10**7asyncdefagen():foriinrange(N):yieldiclassAIter:def__init__(self):self.i=0def__aiter__(self):returnselfasyncdef__anext__(self):i=self.iifi>=N:raiseStopAsyncIterationself.i+=1returni
aiter() andanext() builtinsOriginally,PEP 492 defined__aiter__ as a method that shouldreturn anawaitable object, resulting in an asynchronous iterator.
However, in CPython 3.5.2,__aiter__ was redefined to returnasynchronous iterators directly. To avoid breaking backwardscompatibility, it was decided that Python 3.6 will support bothways:__aiter__ can still return anawaitable withaDeprecationWarning being issued.
Because of this dual nature of__aiter__ in Python 3.6, we cannotadd a synchronous implementation ofaiter() built-in. Therefore,it is proposed to wait until Python 3.7.
Syntax for asynchronous comprehensions is unrelated to the asynchronousgenerators machinery, and should be considered in a separate PEP.
yieldfromWhile it is theoretically possible to implementyieldfrom supportfor asynchronous generators, it would require a serious redesign of thegenerators implementation.
yieldfrom is also less critical for asynchronous generators, sincethere is no need provide a mechanism of implementing another coroutinesprotocol on top of coroutines. And to compose asynchronous generators asimpleasyncfor loop can be used:
asyncdefg1():yield1yield2asyncdefg2():asyncforving1():yieldv
asend() andathrow() methods are necessaryThey make it possible to implement concepts similar tocontextlib.contextmanager using asynchronous generators.For instance, with the proposed design, it is possible to implementthe following pattern:
@async_context_managerasyncdefctx():awaitopen()try:yieldfinally:awaitclose()asyncwithctx():await...
Another reason is that it is possible to push data and throw exceptionsinto asynchronous generators using the object returned from__anext__ object, but it is hard to do that correctly. Addingexplicitasend() andathrow() will pave a safe way toaccomplish that.
In terms of implementation,asend() is a slightly more genericversion of__anext__, andathrow() is very similar toaclose(). Therefore, having these methods defined for asynchronousgenerators does not add any extra complexity.
A working example with the current reference implementation (willprint numbers from 0 to 9 with one second delay):
asyncdefticker(delay,to):foriinrange(to):yieldiawaitasyncio.sleep(delay)asyncdefrun():asyncforiinticker(1,10):print(i)importasyncioloop=asyncio.get_event_loop()try:loop.run_until_complete(run())finally:loop.close()
The implementation is tracked in issue 28003[3]. The referenceimplementation git repository is available at[1].
I thank Guido van Rossum, Victor Stinner, Elvis Pranskevichus,Nathaniel Smith, Łukasz Langa, Andrew Svetlov and many othersfor their feedback, code reviews, and discussions around thisPEP.
This document has been placed in the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-0525.rst
Last modified:2025-02-01 08:59:27 GMT