Developing with asyncio

Python documentation

Asynchronous programming is different from classic “sequential” programming.

This page lists common mistakes and traps and explains how to avoid them.

Debug Mode

By default asyncio runs in production mode. In order to ease the development asyncio has a debug mode.

There are several ways to enable asyncio debug mode:

Setting the

PYTHONASYNCIODEBUG

environment variable to 1.

Using the

Python Development Mode

.

Passing debug=True to

asyncio.run()

.

Calling

loop.set_debug()

.

In addition to enabling the debug mode, consider also:

setting the log level of the

asyncio logger

to

logging.DEBUG

, for example the following snippet of code can be run at startup of the application:

logging.basicConfig(level=logging.DEBUG)

configuring the

warnings

module to display

ResourceWarning

warnings. One way of doing that is by using the

-W

default command line option.

When the debug mode is enabled:

asyncio checks for

coroutines that were not awaited

and logs them; this mitigates the “forgotten await” pitfall.

Many non-threadsafe asyncio APIs (such as

loop.call_soon()

and

loop.call_at()

methods) raise an exception if they are called from a wrong thread.

The execution time of the I/O selector is logged if it takes too long to perform an I/O operation.

Callbacks taking longer than 100 milliseconds are logged. The

loop.slow_callback_duration

attribute can be used to set the minimum execution duration in seconds that is considered “slow”.

Concurrency and Multithreading

An event loop runs in a thread (typically the main thread) and executes all callbacks and Tasks in its thread. While a Task is running in the event loop, no other Tasks can run in the same thread. When a Task executes an await expression, the running Task gets suspended, and the event loop executes the next Task.

To schedule a

callback

from another OS thread, the

loop.call_soon_threadsafe()

method should be used. Example:

loop.call_soon_threadsafe(callback,*args)Almost all asyncio objects are not thread safe, which is typically not a problem unless there is code that works with them from outside of a Task or a callback. If there’s a need for such code to call a low-level asyncio API, the

loop.call_soon_threadsafe()

method should be used, e.g.:

loop.call_soon_threadsafe(fut.cancel)To schedule a coroutine object from a different OS thread, the

run_coroutine_threadsafe()

function should be used. It returns a

concurrent.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 be run in the main thread.

The

loop.run_in_executor()

method can be used with a

concurrent.futures.ThreadPoolExecutor

to execute blocking code in a different OS thread without blocking the OS thread that the event loop runs in.

There is currently no way to schedule coroutines or callbacks directly from a different process (such as one started with

multiprocessing

). The

Event Loop Methods

section lists APIs that can read from pipes and watch file descriptors without blocking the event loop. In addition, asyncio’s

Subprocess

APIs provide a way to start a process and communicate with it from the event loop. Lastly, the aforementioned

loop.run_in_executor()

method can also be used with a

concurrent.futures.ProcessPoolExecutor

to execute code 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 delayed by 1 second.

An executor can be used to run a task in a different thread or even in a different process to avoid blocking the OS thread with the event loop. See the

loop.run_in_executor()

method for more details.

Logging

asyncio uses the

logging

module and all logging is performed via the "asyncio" logger.

The default log level is

logging.INFO

, which can be easily adjusted:

logging.getLogger("asyncio").setLevel(logging.WARNING)Network logging can block the event loop. It is recommended to use a separate thread for handling logs or use non-blocking IO. For example, see

Dealing with handlers that block

.

Detect never-awaited coroutines

When a coroutine function is called, but not awaited (e.g. coro() instead of awaitcoro()) or the coroutine is not scheduled with

asyncio.create_task()

, asyncio will emit a

RuntimeWarning

:

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 the

asyncio.create_task()

function:

asyncdefmain():awaittest()Detect never-retrieved exceptions

If a

Future.set_exception()

is called but the Future object is never awaited on, the exception would never be propagated to the user code. In this case, asyncio would emit a log message when the Future 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 the traceback 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