"How does Python's asyncio work? What's the difference between a coroutine, a Task, and a Future?"
Asyncio is a deep Python question that separates candidates who can write async def from those who understand what's happening underneath. It's essential for backend, data, and ML serving roles where I/O-bound concurrency matters.
Start with the model: asyncio is single-threaded cooperative concurrency. One event loop runs one coroutine at a time; coroutines voluntarily yield on await so the loop can run something else.
Coroutine: the object you get from calling an async def function without awaiting it. It's suspended code — holds local state but isn't running yet. You await it or wrap it in a Task to make it run.
Future: low-level placeholder for a result that will be available later. Has states pending / finished / cancelled. You don't create Futures directly in app code; the library does it.
Task: a subclass of Future that wraps a coroutine and schedules it on the event loop. Created via asyncio.create_task() or implicitly by asyncio.gather(). Tasks run concurrently on the same thread.
The loop mechanic: every await suspends the current coroutine and returns control to the loop. The loop looks for ready callbacks (I/O-completed, timers fired) and resumes the next ready coroutine. Under the hood, this is an epoll/kqueue-style selector polling file descriptors plus a scheduler.
Asyncio is Python's built-in framework for single-threaded cooperative concurrency — one thread, one event loop, many coroutines taking turns. The distinction between coroutine, Task, and Future trips up most people. A coroutine is the object you get when you call an async def function without awaiting: async def foo(): ... ; c = foo() returns a coroutine object, not a result. It's suspended code — it holds its local state but isn't executing. To make it run, you either await it from another coroutine (sequentially) or wrap it in a Task to run concurrently. A Future is a low-level placeholder: "there will be a value here eventually". It has states — pending, finished, cancelled — and a result that's set once. You rarely create Futures in app code; lower-level libraries do. A Task is a subclass of Future that wraps a coroutine and registers it with the event loop so it runs concurrently with other tasks. You create tasks with asyncio.create_task(coro) or implicitly via asyncio.gather(*coros). Gather returns a list of results when all tasks complete. The event loop itself is a scheduler-plus-selector. Every await point either returns immediately if the awaited value is already available, or tells the loop "wake me when this file descriptor / timer / Future is ready". The loop sleeps in an epoll call (Linux) until any registered event fires, then resumes the corresponding coroutine. Because everything is single-threaded, there's no GIL contention between coroutines — they literally take turns. This makes asyncio great for I/O-bound work — thousands of concurrent HTTP requests or database queries — but useless for CPU-bound work, which still blocks the single thread. The canonical mistake is calling a blocking function inside a coroutine (requests.get, time.sleep, a sync DB driver) — it freezes the entire loop and every other coroutine waiting on it. For CPU work, use asyncio.to_thread or run_in_executor to shove the blocking call onto a threadpool.
The coroutine / Task / Future distinction is what interviewers want — answer it explicitly.
Always mention that asyncio is single-threaded and for I/O-bound work — confusing it with threading is a red flag.
Calling out the "blocking call freezes the loop" failure mode shows real-world experience.
If you blank on to_thread / run_in_executor in the interview, HireMe AI can surface these helpers.
For I/O-bound work with many concurrent connections, yes — no context switch cost, minimal memory per coroutine. For CPU work, neither helps because of the GIL; use multiprocessing.
Yes, with care. Sync code calling async: asyncio.run(coro()) from a sync context. Async calling sync: await asyncio.to_thread(blocking_fn) to avoid freezing the loop.
gather collects results and raises on first exception (by default); wait gives you more control — you can wait for FIRST_COMPLETED or FIRST_EXCEPTION. Gather is the common choice.
HireMe AI listens to your interview in real-time, detects questions, and generates instant answer suggestions — so you never freeze up again.
Try HireMe AI Free