Async bugs are often silent; debug mode turns them into visible warnings with tracebacks.
Enable debug mode with PYTHONASYNCIODEBUG=1, python -X dev, or asyncio.run(main(), debug=True). It then:
- logs callbacks or task steps that take longer than
loop.slow_callback_duration(default 0.1 s), pointing straight at blocking code, - records where each coroutine and task was created, so "coroutine was never awaited" and "Task exception was never retrieved" messages show the creation traceback,
- raises an error when non-threadsafe loop methods are called from another thread,
- logs slow I/O selector calls.
Turn on the asyncio logger (logging.basicConfig(level=logging.DEBUG)) to see these messages, and -X dev or -W default to see ResourceWarnings for unclosed transports.
The usual suspects it reveals:
- forgotten
await, - blocking calls inside coroutines,
- fire-and-forget tasks whose exceptions nobody reads,
- objects (Lock, Queue) created in one loop and used in another, often between test cases.
At runtime, asyncio.all_tasks() lists live tasks, and task.print_stack() shows where each is suspended, handy for diagnosing hangs. A custom handler via loop.set_exception_handler can route unhandled errors to your error tracker.
Example
import asyncio, logging, time
logging.basicConfig(level=logging.WARNING, format="%(name)s: %(message).60s")
async def blocker():
time.sleep(0.15) # blocking call in a coroutine
async def main():
loop = asyncio.get_running_loop()
loop.slow_callback_duration = 0.1
await asyncio.create_task(blocker(), name="blocker")
print("live tasks:", [t.get_name() for t in asyncio.all_tasks()])
asyncio.run(main(), debug=True)
# asyncio: Executing <Task finished name='blocker' coro=<blocker() don...
# live tasks: ['Task-1']Remember
- debug=True / PYTHONASYNCIODEBUG=1 / -X dev turn on debug mode
- It logs slow callbacks, never-awaited coroutines with creation tracebacks
- all_tasks() and task.print_stack() help diagnose hangs
Watch out for
- Leaving debug mode on in production; it adds noticeable overhead.