A strong answer
An async def function is a native coroutine built on the exact same heap-allocated frame machinery as a generator (PyCoroObject, driven via .send(None)). The asyncio Event Loop runs on a single OS thread using three components: (1) Future: A state box (PENDING -> FINISHED) holding a list of done-callbacks. (2) Task: A wrapper around a top-level coroutine whose Task.__step() method calls coro.send(None). When the coroutine hits await sock_read(), the bottom-level I/O function creates a Future, registers the socket file descriptor with the OS kernel's epoll (Linux) / kqueue (macOS) multiplexer via loop.add_reader(fd, future.set_result), and yields that Future all the way up to Task.__step()! Task.__step() attaches self.__wakeup as a callback on the Future and returns control to the event loop immediately so other ready tasks can run. (3) When the OS kernel signals that fd is readable, the event loop calls future.set_result(), which schedules Task.__wakeup -> Task.__step(), resuming the coroutine right after the await!
A single-threaded asyncio event loop can switch between tasks ONLY at explicit await expressions that yield control back to the loop; any synchronous blocking call (time.sleep, requests.get, heavy CPU math) monopolizes the single OS thread and halts all other tasks. Because asyncio uses cooperative multitasking on one OS thread without preemptive timer interrupts, loop._run_once() cannot dequeue the next ready callback from loop._ready until the currently executing Task.__step() call returns.
An asyncio.Task + coroutine frame consumes only ~2–4 KB of RAM (compared to 8 MB default virtual stack for an OS thread) and switches in ~150–300 nanoseconds in user space without kernel thread context switches. In production systems (uvloop (libuv Cython event loop), Uvicorn, FastAPI, aiohttp, httpx.AsyncClient, and asyncpg (which parses PostgreSQL wire protocol directly in Cython over non-blocking sockets) all run on this exact Task.__step + epoll architecture.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how the
asyncioEvent Loop,Task.__steptrampoline, and OSepoll/kqueuereadiness polling operates in memory step by step rather than giving a vague textbook definition. - States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (OS Thread (
threading.Thread),asyncio.Task+ Coroutine) with concrete numbers. - Identifies real-world failure modes (Calling
asyncio.create_task(coro)as a 'fire-and-forget' call without saving a strong reference to the returnedTask) and explains how production systems mitigate them.
Common mistakes
- Treating the
asyncioEvent Loop,Task.__steptrampoline, and OSepoll/kqueuereadiness polling as a black box without explaining the underlying memory or state transitions. - Ignoring worst-case tail latency or memory overhead (Calling
requests.get(),time.sleep(), or synchronousboto3/psycopg2insideasync def). - Forgetting boundary conditions and error recovery (Writing
for url in urls: res = await fetch(url)thinkingawaitmakes the loop parallel).