Developing with asyncio¶
Asynchronous programming is different from classic “sequential”programming.
This page lists common mistakes and traps and explains howto avoid them.
Debug Mode¶
By default asyncio runs in production mode. In order to easethe development asyncio has adebug mode.
There are several ways to enable asyncio debug mode:
Setting the
PYTHONASYNCIODEBUGenvironment variable to1.Using thePython Development Mode.
Passing
debug=Truetoasyncio.run().Calling
loop.set_debug().
In addition to enabling the debug mode, consider also:
setting the log level of theasyncio logger to
logging.DEBUG, for example the following snippet of codecan be run at startup of the application:logging.basicConfig(level=logging.DEBUG)
configuring the
warningsmodule to displayResourceWarningwarnings. One way of doing that is byusing the-Wdefaultcommand line option.
When the debug mode is enabled:
Many non-threadsafe asyncio APIs (such as
loop.call_soon()andloop.call_at()methods) raise an exception if they are calledfrom a wrong thread.The execution time of the I/O selector is logged if it takes too long toperform an I/O operation.
Callbacks taking longer than 100 milliseconds are logged. The
loop.slow_callback_durationattribute can be used to set theminimum execution duration in seconds that is considered “slow”.
Concurrency and Multithreading¶
An event loop runs in a thread (typically the main thread) and executesall callbacks and Tasks in its thread. While a Task is running in theevent loop, no other Tasks can run in the same thread. When a Taskexecutes anawait expression, the running Task gets suspended, andthe event loop executes the next Task.
To schedule acallback from another OS thread, theloop.call_soon_threadsafe() method should be used. Example:
loop.call_soon_threadsafe(callback,*args)
Almost all asyncio objects are not thread safe, which is typicallynot a problem unless there is code that works with them from outsideof a Task or a callback. If there’s a need for such code to call alow-level asyncio API, theloop.call_soon_threadsafe() methodshould be used, e.g.:
loop.call_soon_threadsafe(fut.cancel)
To schedule a coroutine object from a different OS thread, therun_coroutine_threadsafe() function should be used. It returns aconcurrent.futures.Future to access the result:
asyncdefcoro_func():returnawaitasyncio.sleep(1,42)# Later in another OS thread:future=asyncio.run_coroutine_threadsafe(coro_func(),loop)# Wait for the result:result=future.result()
To handle signals the event loop must berun in the main thread.
Theloop.run_in_executor() method can be used with aconcurrent.futures.ThreadPoolExecutor orInterpreterPoolExecutor to executeblocking code in a different OS thread without blocking the OS threadthat the event loop runs in.
There is currently no way to schedule coroutines or callbacks directlyfrom a different process (such as one started withmultiprocessing). TheEvent Loop Methodssection lists APIs that can read from pipes and watch file descriptorswithout blocking the event loop. In addition, asyncio’sSubprocess APIs provide a way to start aprocess and communicate with it from the event loop. Lastly, theaforementionedloop.run_in_executor() method can also be usedwith aconcurrent.futures.ProcessPoolExecutor to executecode in a different process.
Running Blocking Code¶
Blocking (CPU-bound) code should not be called directly. For example,if a function performs a CPU-intensive calculation for 1 second,all concurrent asyncio Tasks and IO operations would be delayedby 1 second.
An executor can be used to run a task in a different thread,including in a different interpreter, or even ina different process to avoid blocking the OS thread with theevent loop. See theloop.run_in_executor() method for moredetails.
Logging¶
asyncio uses thelogging module and all logging is performedvia the"asyncio" logger.
The default log level islogging.INFO, which can be easilyadjusted:
logging.getLogger("asyncio").setLevel(logging.WARNING)
Network logging can block the event loop. It is recommended to usea separate thread for handling logs or use non-blocking IO. For example,seeDealing with handlers that block.
Detect never-awaited coroutines¶
When a coroutine function is called, but not awaited(e.g.coro() instead ofawaitcoro())or the coroutine is not scheduled withasyncio.create_task(), asynciowill emit aRuntimeWarning:
importasyncioasyncdeftest():print("never scheduled")asyncdefmain():test()asyncio.run(main())
Output:
test.py:7:RuntimeWarning:coroutine'test'wasneverawaitedtest()
Output in debug mode:
test.py:7:RuntimeWarning:coroutine'test'wasneverawaitedCoroutinecreatedat(mostrecentcalllast)File"../t.py",line9,in<module>asyncio.run(main(),debug=True)<..>File"../t.py",line7,inmaintest()test()
The usual fix is to either await the coroutine or call theasyncio.create_task() function:
asyncdefmain():awaittest()
Detect never-retrieved exceptions¶
If aFuture.set_exception() is called but the Future object isnever awaited on, the exception would never be propagated to theuser code. In this case, asyncio would emit a log message when theFuture object is garbage collected.
Example of an unhandled exception:
importasyncioasyncdefbug():raiseException("not consumed")asyncdefmain():asyncio.create_task(bug())asyncio.run(main())
Output:
Taskexceptionwasneverretrievedfuture:<Taskfinishedcoro=<bug()done,definedattest.py:3>exception=Exception('not consumed')>Traceback(mostrecentcalllast):File"test.py",line4,inbugraiseException("not consumed")Exception:notconsumed
Enable the debug mode to get thetraceback where the task was created:
asyncio.run(main(),debug=True)
Output in debug mode:
Taskexceptionwasneverretrievedfuture:<Taskfinishedcoro=<bug()done,definedattest.py:3>exception=Exception('not consumed')createdatasyncio/tasks.py:321>source_traceback:Objectcreatedat(mostrecentcalllast):File"../t.py",line9,in<module>asyncio.run(main(),debug=True)<..>Traceback(mostrecentcalllast):File"../t.py",line4,inbugraiseException("not consumed")Exception:notconsumed
Asynchronous generators best practices¶
Writing correct and efficient asyncio code requires awareness of certain pitfalls.This section outlines essential best practices that can save you hours of debugging.
Close asynchronous generators explicitly¶
It is recommended to manually close theasynchronous generator. If a generatorexits early - for example, due to an exception raised in the body ofanasyncfor loop - its asynchronous cleanup code may run in anunexpected context. This can occur after the tasks it depends on have completed,or during the event loop shutdown when the async-generator’s garbage collectionhook is called.
To avoid this, explicitly close the generator by calling itsaclose() method, or use thecontextlib.aclosing()context manager:
importasyncioimportcontextlibasyncdefgen():yield1yield2asyncdeffunc():asyncwithcontextlib.aclosing(gen())asg:asyncforxing:break# Don't iterate until the endasyncio.run(func())
As noted above, the cleanup code for these asynchronous generators is deferred.The following example demonstrates that the finalization of an asynchronousgenerator can occur in an unexpected order:
importasynciowork_done=Falseasyncdefcursor():try:yield1finally:assertwork_doneasyncdefrows():globalwork_donetry:yield2finally:awaitasyncio.sleep(0.1)# immitate some async workwork_done=Trueasyncdefmain():asyncforcincursor():asyncforrinrows():breakbreakasyncio.run(main())
For this example, we get the following output:
unhandledexceptionduringasyncio.run()shutdowntask:<Taskfinishedname='Task-3'coro=<<async_generator_athrowwithout__name__>()>exception=AssertionError()>Traceback(mostrecentcalllast):File"example.py",line6,incursoryield1asyncio.exceptions.CancelledErrorDuringhandlingoftheaboveexception,anotherexceptionoccurred:Traceback(mostrecentcalllast):File"example.py",line8,incursorassertwork_done^^^^^^^^^AssertionError
Thecursor() asynchronous generator was finalized before therowsgenerator - an unexpected behavior.
The example can be fixed by explicitly closing thecursor androws async-generators:
asyncdefmain():asyncwithcontextlib.aclosing(cursor())ascursor_gen:asyncforcincursor_gen:asyncwithcontextlib.aclosing(rows())asrows_gen:asyncforrinrows_gen:breakbreak
Create asynchronous generators only when the event loop is running¶
It is recommended to createasynchronous generators only afterthe event loop has been created.
To ensure that asynchronous generators close reliably, the event loop uses thesys.set_asyncgen_hooks() function to register callback functions. Thesecallbacks update the list of running asynchronous generators to keep it in aconsistent state.
When theloop.shutdown_asyncgens()function is called, the running generators are stopped gracefully and thelist is cleared.
The asynchronous generator invokes the corresponding system hook during itsfirst iteration. At the same time, the generator records that the hook hasbeen called and does not call it again.
Therefore, if iteration begins before the event loop is created,the event loop will not be able to add the generator to its list of activegenerators because the hooks are set after the generator attempts to call them.Consequently, the event loop will not be able to terminate the generatorif necessary.
Consider the following example:
importasyncioasyncdefagenfn():try:yield10finally:awaitasyncio.sleep(0)withasyncio.Runner()asrunner:agen=agenfn()print(runner.run(anext(agen)))delagen
Output:
10Exceptionignoredwhileclosinggenerator<async_generatorobjectagenfnat0x000002F71CD10D70>:Traceback(mostrecentcalllast):File"example.py",line13,in<module>delagen^^^^RuntimeError:asyncgeneratorignoredGeneratorExit
This example can be fixed as follows:
importasyncioasyncdefagenfn():try:yield10finally:awaitasyncio.sleep(0)asyncdefmain():agen=agenfn()print(awaitanext(agen))delagenasyncio.run(main())
Avoid concurrent iteration and closure of the same generator¶
Async generators may be reentered while another__anext__() /athrow() /aclose() call is inprogress. This may lead to an inconsistent state of the async generator and cancause errors.
Let’s consider the following example:
importasyncioasyncdefconsumer():foridxinrange(100):awaitasyncio.sleep(0)message=yieldidxprint('received',message)asyncdefamain():agenerator=consumer()awaitagenerator.asend(None)fa=asyncio.create_task(agenerator.asend('A'))fb=asyncio.create_task(agenerator.asend('B'))awaitfaawaitfbasyncio.run(amain())
Output:
receivedATraceback(mostrecentcalllast):File"test.py",line38,in<module>asyncio.run(amain())~~~~~~~~~~~^^^^^^^^^File"Lib/asyncio/runners.py",line204,inrunreturnrunner.run(main)~~~~~~~~~~^^^^^^File"Lib/asyncio/runners.py",line127,inrunreturnself._loop.run_until_complete(task)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^File"Lib/asyncio/base_events.py",line719,inrun_until_completereturnfuture.result()~~~~~~~~~~~~~^^File"test.py",line36,inamainawaitfbRuntimeError:anext():asynchronousgeneratorisalreadyrunning
Therefore, it is recommended to avoid using asynchronous generators in paralleltasks or across multiple event loops.