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:

In addition to enabling the debug mode, consider also:

  • setting the log level of theasyncio logger tologging.DEBUG, for example the following snippet of codecan be run at startup of the application:

    logging.basicConfig(level=logging.DEBUG)
  • configuring thewarnings module to displayResourceWarning warnings. One way of doing that is byusing the-Wdefault command line option.

When the debug mode is enabled:

  • Many non-threadsafe asyncio APIs (such asloop.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. Theloop.slow_callback_duration attribute 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 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 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