asyncio & async/await Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on asyncio and async/await: the event loop, coroutines vs threads, asyncio.gather, the don't-block-the-loop rule, and when async helps.
asyncio is Python's framework for single-threaded concurrency using an event loop. The event loop is a scheduler that runs many coroutines cooperatively: when one coroutine awaits something (typically I/O), it yields control back to the loop, which runs another ready coroutine while the first waits. No thread is blocked sitting idle.
import asyncio
async def main():
print("hello")
await asyncio.sleep(1) # yields to the loop instead of blocking
print("world")
asyncio.run(main()) # creates the loop, runs main, then closes it
Crucially this is concurrency, not parallelism — one thread, one core,
interleaving tasks at await points. Rule of thumb: asyncio shines when you
have many tasks that spend most of their time waiting on I/O.
async def defines a coroutine function — calling it doesn't run the
body, it returns a coroutine object that must be awaited or scheduled.
await suspends the current coroutine until the awaited awaitable
(another coroutine, a Task, or a Future) completes, handing control back to the
event loop in the meantime.
import asyncio
async def fetch():
await asyncio.sleep(1) # suspension point — loop runs others here
return "data"
async def main():
coro = fetch() # nothing has run yet
result = await coro # now it runs; main suspends until it finishes
print(result)
asyncio.run(main())
You can only await inside an async def. Forgetting to await a coroutine
is a common bug — it never runs and you get a "coroutine was never awaited"
warning. Think of await as "pause me here and let others run until this is
ready."
Threads use preemptive multitasking — the OS can switch threads at
any point, so shared state needs locks and context switches are relatively
expensive. Coroutines use cooperative multitasking on one thread —
switches happen only at explicit await points, so the code between awaits
is effectively atomic and switching is cheap.
import asyncio
async def task(name):
print(f"{name} start")
await asyncio.sleep(1) # the ONLY place this can yield
print(f"{name} done")
async def main():
await asyncio.gather(task("a"), task("b")) # thousands are feasible
asyncio.run(main())
Because there's no OS thread per task, you can run tens of thousands of coroutines cheaply, and most data races disappear. The catch: a coroutine that never awaits monopolizes the loop. Threads tolerate blocking code; coroutines do not.
Awaiting coroutines one by one runs them sequentially. To run them
concurrently, schedule them together with asyncio.gather (or wrap each
in a Task), which lets the loop interleave their await points.
import asyncio
async def fetch(n):
await asyncio.sleep(1)
return n * 2
async def main():
# all three overlap -> ~1 second total, not 3
results = await asyncio.gather(fetch(1), fetch(2), fetch(3))
print(results) # [2, 4, 6] — order matches the arguments
asyncio.run(main())
gather returns results in argument order and, by default, propagates the first
exception. asyncio.create_task(coro) schedules a coroutine to start running
immediately so it overlaps with later code. The key idea: concurrency comes from
scheduling tasks together, not from awaiting them in turn.
Because asyncio runs on one thread, any code that doesn't await —
heavy CPU work or blocking synchronous I/O like time.sleep,
requests.get, or blocking DB drivers — freezes the entire loop. Every other
coroutine stalls until that call returns.
import asyncio, time
async def bad():
time.sleep(5) # BLOCKS the whole loop for 5s
async def good():
await asyncio.sleep(5) # yields; other tasks keep running
# offload unavoidable blocking/CPU work to a thread or process pool:
async def offloaded():
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, time.sleep, 5) # runs in a thread
Fixes: use async-native libraries (aiohttp, asyncpg), and push
CPU-bound or unavoidably-blocking calls into run_in_executor (a thread pool, or
a process pool for CPU work). Rule of thumb: inside async code, never call
something that blocks without awaiting it.
asyncio wins for high-concurrency I/O-bound workloads — thousands of network calls, web requests, websocket connections, or database queries that spend their time waiting. While one request waits, the loop services others, so a single thread handles huge concurrency cheaply.
import asyncio
async def call_api(i):
await asyncio.sleep(0.5) # stand-in for a network round trip
return i
async def main():
# 1000 "requests" overlap on one thread in ~0.5s of wall time
results = await asyncio.gather(*(call_api(i) for i in range(1000)))
print(len(results)) # 1000
asyncio.run(main())
It does not help CPU-bound work — that needs multiprocessing for real parallelism. And for a handful of blocking calls, plain threads are often simpler. Reach for asyncio when concurrency is high and the bottleneck is waiting on I/O.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.