Most developers learn how to write asynchronous code very quickly:
async def get_user():
return {"id": 1}
However, very few understand what actually happens behind the scenes when Python encounters an async function. This lack of understanding often becomes visible during interviews.
Interviewers frequently ask questions such as:
- What is a coroutine?
- What happens when an async function is called?
- What is the Event Loop?
- What is the difference between a Task and a Future?
- Why does FastAPI perform better for I/O workloads?
- Does async code run in parallel?
These questions are designed to evaluate whether a candidate understands asynchronous programming beyond syntax.
In this article, we will explore asyncio from first principles and understand how coroutines, tasks, futures, and the event loop work together to provide concurrency in Python.
Why Asyncio Exists
Before asyncio, Python applications typically handled concurrent work using threads. Consider a service that retrieves user information from a database.def get_user():
data = database.fetch_user()
return data
While waiting for the database response, the thread remains idle. The CPU is not performing useful work, yet the thread continues to occupy memory and operating system resources.
For a handful of requests this is not a problem. For thousands of concurrent requests it becomes expensive.
Modern backend systems spend a significant amount of time waiting for external resources such as databases, HTTP APIs, Redis, Kafka, file systems, and network communication. These operations are collectively known as I/O-bound operations.
Asyncio was created to allow applications to perform useful work while waiting for I/O operations to complete.
Understanding Concurrency vs Parallelism
Many developers incorrectly assume that asynchronous programming automatically means parallel execution. This is not true.- Concurrency means multiple tasks make progress during the same period of time.
- Parallelism means multiple tasks execute simultaneously.
Consider the following analogy. Imagine a chef preparing three dishes.
- A synchronous chef waits for one dish to finish before starting another.
- An asynchronous chef starts cooking one dish, performs another task while waiting, and returns later.
- A parallel kitchen uses multiple chefs working at the same time.
Asyncio provides concurrency, not parallelism. Most asyncio applications execute on a single thread using cooperative task switching.
Your First Coroutine
The foundation of asyncio is the coroutine.A coroutine is a concurrency design pattern used to execute multiple tasks asynchronously without blocking threads. Often described as "lightweight threads" or "functions you can pause," they allow you to write complex, non-blocking asynchronous code in a clean, sequential style.
A coroutine is created using the
async def keyword. Example:
async def fetch_user():
return {
"id": 1,
"name": "John"
}
Many developers assume that calling this function immediately executes it. Let's see what actually happens.
result = fetch_user()
print(result)
Output:
<coroutine object fetch_user>
The function does not execute. Instead, Python creates a coroutine object. A coroutine is essentially a suspended piece of work waiting to be scheduled by the event loop.
The Event Loop
The Event Loop is the heart of asyncio. Every asyncio application contains an event loop that is responsible for scheduling tasks, resuming paused coroutines, monitoring I/O operations, and managing execution order.Whenever a coroutine reaches an await point, control returns to the event loop. The event loop then schedules another coroutine that is ready to execute.
This allows many operations to make progress without requiring thousands of threads.
Understanding await
Theawait keyword is one of the most important concepts in asyncio. Example:
import asyncio
async def process():
print("Start")
await asyncio.sleep(5)
print("End")
When Python reaches await asyncio.sleep(5), the coroutine pauses and yields control back to the event loop.
The event loop is now free to execute other tasks while this coroutine waits. This is the key mechanism that enables concurrency.
What Happens Inside FastAPI?
Consider the following endpoint.@app.get("/users")
async def get_users():
users = await database.fetch_users()
return users

Understanding Tasks
A coroutine becomes executable when it is wrapped inside a Task. Example:import asyncio
async def work():
return "done"
task = asyncio.create_task(work())
A Task is a wrapper around a coroutine that allows the event loop to manage and schedule execution.
Think of it as a scheduled coroutine. Without a task, a coroutine simply exists. With a task, the event loop can execute it.
Running Multiple Tasks Concurrently
Consider three API calls.async def service_a():
await asyncio.sleep(2)
async def service_b():
await asyncio.sleep(2)
async def service_c():
await asyncio.sleep(2)
Sequential execution:
await service_a()
await service_b()
await service_c()
Total execution time: ~6 seconds.
Concurrent execution:
await asyncio.gather(
service_a(),
service_b(),
service_c()
)
Total execution time: ~2 seconds. Because all operations wait concurrently.
Understanding Futures
A Future represents a value that may become available later. Simplified example:future = asyncio.Future()
Initially:
Result Not Available
Later:
future.set_result("completed")
Now the result exists. Tasks internally use Futures to track completion. In practice, developers interact with Tasks far more often than raw Futures.
Asyncio Does Not Help CPU-Bound Workloads
Consider:async def calculate():
total = 0
for i in range(100000000):
total += i
return total
No I/O exists.
- The CPU remains busy the entire time.
- The coroutine never yields control.
- Asyncio provides little benefit here.
Asyncio is extremely effective for I/O-bound workloads but provides minimal benefit for CPU-intensive operations. For CPU-heavy workloads, consider using multiprocessing, process pools, or distributed workers.
Common Asyncio Mistakes
One of the most common mistakes is calling blocking functions inside asynchronous code. Example:import time
async def bad():
time.sleep(5)
This blocks the event loop.
A better approach:
import asyncio
async def good():
await asyncio.sleep(5)
The second version allows other tasks to execute while waiting.
Interview Questions and Answers
What is a coroutine? - A coroutine is a function defined using async def that can be paused and resumed by the event loop.What is the Event Loop? - The Event Loop is the scheduler responsible for executing coroutines, managing I/O operations, and resuming suspended tasks.
What happens when an async function is called? - Python creates a coroutine object. The function does not execute immediately.
What is a Task? - A Task is a scheduled coroutine managed by the event loop.
What is a Future? - A Future represents a result that will become available later.
Does asyncio provide parallelism? - No. Asyncio primarily provides concurrency.
Why does FastAPI perform well? - Because asynchronous endpoints can yield control while waiting for I/O operations, allowing the event loop to process other requests.
Conclusion
Asyncio is the foundation of modern asynchronous Python applications and plays a critical role in FastAPI.Coroutines define units of asynchronous work, Tasks schedule those coroutines for execution, Futures represent results that will become available later, and the Event Loop coordinates everything.