Async & Concurrency
Non‑Blocking Python
Write high‑performance, concurrent code with asyncio. Master coroutines, event loops, tasks, and real‑world async patterns — all free, all in your browser.
import asyncio async def fetch_data(url: str) -> str: print(f"Fetching {url}...") await asyncio.sleep(1) return f"Data from {url}" async def main(): urls = ["api.one", "api.two", "api.three"] tasks = [fetch_data(url) for url in urls] results = await asyncio.gather(*tasks) for res in results: print(res) asyncio.run(main())
Fetching api.one...
Fetching api.two...
Fetching api.three...
Data from api.one
Data from api.two
Data from api.three
📑 Table of Contents
Jump to any section instantly
Why Async Python?
Concurrency without threads — handle thousands of I/O operations simultaneously
In traditional synchronous code, each I/O operation (network request, file read, database query) blocks the entire program until it completes. This wastes CPU time and limits scalability. Async Python lets you write code that pauses only when waiting for I/O, allowing other tasks to run in the meantime — all within a single thread.
Async ≠ Parallelism
Async is concurrency, not parallelism. It efficiently manages many I/O‑bound tasks on one thread. For CPU‑bound work, use multiprocessing. Async shines in web servers, scrapers, and API clients.
Core Async Concepts
Coroutines, event loops, tasks, and await — explained with code
🌀 Coroutines — async def and await
An async def function is a coroutine. When called, it returns a coroutine object, not the result. You must await it to get the value. Inside a coroutine, await pauses execution until the awaited object (like a future or another coroutine) is ready.
async def say_hello(name: str) -> str: await asyncio.sleep(0.1) return f"Hello, {name}!" # Coroutine object — not executed yet coro = say_hello("Glenn") # To run, await inside another coroutine or use asyncio.run() async def main(): result = await say_hello("Glenn") print(result) asyncio.run(main())
await can only be used inside async def. It suspends the current coroutine, giving the event loop a chance to run other tasks.🔄 Event Loop — The Heart of asyncio
The event loop manages all coroutines, schedules their execution, and handles I/O readiness. You get the loop with asyncio.get_running_loop() (inside a coroutine) or asyncio.new_event_loop(). In most applications, asyncio.run() creates and runs a loop for you.
async def show_loop(): loop = asyncio.get_running_loop() print(f"Loop: {loop}") asyncio.run(show_loop())
📋 Tasks — Wrapping Coroutines for Concurrent Execution
A Task schedules a coroutine to run on the event loop. Use asyncio.create_task() to start a coroutine "in the background" without waiting for it immediately. Tasks allow multiple coroutines to run concurrently.
async def count(label: str, n: int): for i in range(n): print(f"{label}: {i}") await asyncio.sleep(0.5) async def main(): task1 = asyncio.create_task(count("A", 3)) task2 = asyncio.create_task(count("B", 3)) await task1 await task2 asyncio.run(main())
B: 0
A: 1
B: 1
A: 2
B: 2
await asyncio.sleep() yields control.Async Patterns & Best Practices
Gather, timeout, shielding, and graceful shutdown — real‑world async code
📦 Gathering Tasks — asyncio.gather()
Run multiple coroutines concurrently and collect their results in a list. If any task fails, the entire gather raises an exception (unless return_exceptions=True).
async def slow_operation(n: int) -> int: await asyncio.sleep(n) return n * 2 async def main(): results = await asyncio.gather( slow_operation(1), slow_operation(2), slow_operation(3) ) print(results) # [2, 4, 6] asyncio.run(main())
⏱️ Timeouts — asyncio.timeout() and asyncio.wait_for()
Set a maximum duration for an async operation. If the timeout is exceeded, TimeoutError is raised. Use asyncio.timeout() (Python 3.11+) for a context manager.
async def maybe_slow(): await asyncio.sleep(5) return "Done" async def main(): try: result = await asyncio.wait_for(maybe_slow(), timeout=2) print(result) except asyncio.TimeoutError: print("Operation timed out") asyncio.run(main())
TimeoutError gracefully — you may need to cancel the underlying task to avoid leaking resources.🛡️ Shielding — asyncio.shield()
Protect a coroutine from being cancelled. Useful for cleanup or critical operations that must complete even when the surrounding task is cancelled.
async def critical_cleanup(): print("Cleaning up...") await asyncio.sleep(0.5) print("Cleanup done.") async def main(): task = asyncio.create_task(critical_cleanup()) await asyncio.sleep(0.1) task.cancel() try: await task except asyncio.CancelledError: print("Task was cancelled") asyncio.run(main())
Cleanup done.
Task was cancelled
Common Async Pitfalls
Mistakes that trip even experienced developers — and how to avoid them
⚠️ Forgetting to await a coroutine
If you call an async def function without await, you get a coroutine object that does nothing. The code will appear to "hang" or silently skip.
async def say(): return "Hello" async def main(): result = say() # No await — returns coroutine object, not "Hello" print(result) #print(await say()) # Correct
Hello
⚠️ Blocking the event loop with synchronous code
Calling time.sleep() or doing heavy CPU work inside a coroutine blocks the entire event loop. Always use await asyncio.sleep() and offload CPU‑bound tasks to a thread or process.
# Bad — blocks the loop async def bad(): time.sleep(2) # Blocks! # Good — yields control async def good(): await asyncio.sleep(2)
asyncio.to_thread() (Python 3.9+) to run it in a separate thread without blocking.⚠️ Not handling cancellation properly
When a task is cancelled, asyncio.CancelledError is raised inside the coroutine. If you catch it without re‑raising, the task may not actually cancel. Always re‑raise or handle cleanup before re‑raising.
async def worker(): try: await asyncio.sleep(10) except asyncio.CancelledError: print("Cleaning up...") raise # mandatory to propagate cancellation
(CancelledError propagated)
Async FAQ
Quick answers to the most common async questions
Is async Python faster than synchronous code?
+For I/O‑bound tasks (network, databases, file I/O), async can significantly improve throughput because it overlaps waiting. For CPU‑bound tasks, it is not faster; use multiprocessing instead.
Can I use async with existing synchronous libraries?
+Yes, but you need to avoid blocking. Use asyncio.to_thread() to run synchronous functions in a thread pool, or look for async‑first alternatives (e.g., aiohttp instead of requests).
What is the difference between asyncio.run() and loop.run_until_complete()?
+asyncio.run() is a high‑level API that creates a new event loop, runs the coroutine, and closes the loop. It is the recommended way for top‑level entry points. loop.run_until_complete() is lower‑level and requires manual loop management.
How do I cancel a task?
+Call task.cancel() and then await task to allow cancellation to propagate. The task will receive a CancelledError inside its coroutine.
When should I use asyncio.gather() vs asyncio.wait()?
+gather() collects results in a list and raises an exception if any task fails. wait() gives more control over when to return (first completed, all completed, etc.) and returns a set of done/pending tasks. Use gather() for simple concurrent execution, wait() for more advanced policies.
📬 Python, weekly
Short lessons, real code, and ecosystem news — every week