async def and await – The Basics
async def creates a coroutine function. await suspends execution until the awaited operation completes.
import asyncio
async def greet(name, delay):
await asyncio.sleep(delay) # Non-blocking wait
print(f"Hello, {name}!")
async def main():
# Run sequentially: takes 3s
await greet("Alice", 1)
await greet("Bob", 2)
asyncio.run(main())asyncio.gather – True Concurrency
Run multiple coroutines concurrently with asyncio.gather().
import asyncio
import time
async def fetch(url, delay):
await asyncio.sleep(delay) # Simulate network I/O
return f"Response from {url}"
async def main():
start = time.time()
# All three run concurrently — total ~2s not 6s
results = await asyncio.gather(
fetch("api.com/users", 2),
fetch("api.com/posts", 1),
fetch("api.com/data", 2)
)
for r in results:
print(r)
print(f"Done in {time.time()-start:.1f}s")
asyncio.run(main())asyncio.create_task – Background Tasks
Tasks run in the background. Useful for fire-and-forget operations.
import asyncio
async def background_worker():
while True:
print("Background: heartbeat")
await asyncio.sleep(5)
async def main():
task = asyncio.create_task(background_worker())
# Do other work while background runs
await asyncio.sleep(0) # Yield control
print("Main: doing work")
task.cancel() # Stop background
asyncio.run(main())Real Async HTTP with aiohttp
Use aiohttp for async HTTP requests. Dramatically faster than requests for concurrent fetching.
# pip install aiohttp
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = [
"https://api.github.com/users/python",
"https://api.github.com/users/torvalds"
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for r in results:
print(r["login"])
asyncio.run(main())async/await: Concurrency Without Threads
asyncio runs many tasks on a single thread by letting each one pause at an await and hand control back to the event loop. It's cooperative: a task only yields when it hits await, so one CPU-heavy loop with no await blocks everything.
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay) # yields to the loop — other tasks run
return f"{name} done"
async def main():
# run concurrently — total time ≈ the SLOWEST, not the sum
results = await asyncio.gather(fetch("a", 2), fetch("b", 1))
print(results)
asyncio.run(main())
| Workload | Use |
|---|---|
| I/O-bound (network, disk, DB) | asyncio — huge win |
| CPU-bound (crunching numbers) | multiprocessing — asyncio won't help |
The catch: async only speeds up waiting. Because of the GIL and the single thread, CPU-bound work still runs serially — and a blocking call (time.sleep, a sync DB driver) freezes the whole loop. Use await asyncio.sleep() and async libraries throughout, or offload heavy work with run_in_executor.
🏋️ Practical Exercise
Get comfortable with asyncio:
- Write an
async defcoroutine thatawaitsasyncio.sleep(1)and prints a message. - Run it with
asyncio.run(). - Use
asyncio.gather()to run three such coroutines concurrently and time the total. - Schedule one coroutine as a background task with
asyncio.create_task().
🔥 Challenge Exercise
Simulate fetching data from five “endpoints”, each represented by a coroutine that sleeps a random amount and returns a result. Run them sequentially and time it, then run them concurrently with asyncio.gather() and compare the total time — demonstrating that concurrency overlaps the waiting. Bonus: add a timeout with asyncio.wait_for() so a slow endpoint is cancelled.
📋 Summary
- Async programming runs many I/O-bound tasks concurrently on a single thread via an event loop.
- A coroutine is defined with
async defand paused/resumed withawait. asyncio.run()starts the event loop and runs a coroutine to completion.asyncio.gather()runs multiple coroutines concurrently and collects their results.asyncio.create_task()schedules a coroutine to run in the background.- Async helps with I/O-bound work (network, disk); for CPU-bound work use multiprocessing.
Interview Questions on Async / Asyncio
- What is asynchronous programming and how does it differ from threading?
- What do the
asyncandawaitkeywords do? - What is a coroutine?
- What is the difference between concurrency and parallelism?
- What does
asyncio.gather()do? - When is async I/O beneficial versus when does it not help?
- What is an event loop?
Related Topics
FAQ
Async uses a single thread and an event loop that switches between tasks at await points — cooperative multitasking with no lock overhead. Threading uses multiple OS threads preempted by the scheduler. Async excels at many concurrent I/O waits; threads can run blocking code without rewriting it.
When the work is I/O-bound — network requests, database calls, file access — where tasks spend most time waiting. Async overlaps that waiting. For CPU-bound work it gives no speedup because only one coroutine runs at a time.
Calling an async def function returns a coroutine object; it does not run until awaited or scheduled. Use await coro(), asyncio.run(coro()), or asyncio.create_task(coro()).
A blocking call inside a coroutine stalls the whole event loop. Offload it with loop.run_in_executor() or asyncio.to_thread() so the loop stays responsive.

