Concurrency
Event Loops
The asyncio event loop internals — how the loop schedules callbacks, manages tasks, and orchestrates concurrent execution.
Interview: Deep understanding of async — shows knowledge of how asyncio actually works under the hood.
The event loop is the heart of asyncio. It's a program construct that runs in a single thread, dispatching callbacks and coroutines. When a coroutine reaches an await point, the event loop takes back control and runs other ready tasks. Understanding the event loop helps debug timing issues and write efficient async code.
How the Event Loop Works
- Ready queue: Tasks that can run immediately (not waiting on I/O)
- I/O polling: The loop checks which I/O operations have completed (using epoll/kqueue/select)
- Timer heap: Scheduled callbacks (from
call_later,sleep) sorted by time - Each iteration: process ready tasks → poll I/O → process expired timers → repeat
Event Loop API
asyncio.run(coro)— creates a new loop, runs the coroutine, closes the loop (preferred entry point)asyncio.get_event_loop()— get the current loop (deprecated in 3.10+, useget_running_loop())asyncio.get_running_loop()— get the currently running loop (from inside a coroutine)loop.run_in_executor(executor, func)— run blocking code in a thread/process pool from async context
Scheduling Callbacks
loop.call_soon(callback)— schedule callback for next iterationloop.call_later(delay, callback)— schedule after a delayloop.call_at(when, callback)— schedule at an absolute time- These are low-level — prefer
asyncio.create_task()for most use cases
Interview Insight
The event loop is single-threaded — it can only run one callback at a time. Blocking the loop (with CPU-heavy work or blocking I/O) starves all other tasks. Use run_in_executor() to offload blocking work to threads while keeping the loop responsive.
Common Pitfall
Never create multiple event loops in the same thread. asyncio.run() creates and closes a loop — calling it twice in the same context can cause "Event loop is closed" errors. For long-running apps, use loop.run_forever().
Use Cases
Integrating blocking libraries — run_in_executor for requests, psycopg2, etc.
Custom scheduling — delayed callbacks, periodic tasks
Graceful shutdown — timeout + cancel pattern for clean application exit
Background work — shield() to protect critical operations from cancellation
Server applications — long-running event loops with lifecycle management
Common Mistakes
Calling asyncio.run() inside an async function — it creates a new loop, causing errors
Blocking the event loop with CPU-heavy work — use run_in_executor instead
Not closing the event loop — resource leaks in long-running applications
Using get_event_loop() in Python 3.10+ — prefer get_running_loop() inside coroutines
Forgetting that the loop runs one task at a time — any blocking call stalls everything