Python Async Programming — asyncio, async/await Explained
Asynchronous programming in Python lets you write concurrent code that handles many I/O-bound tasks efficiently. Instead of waiting for one task to finish (like a network request), the event loop pauses that task and runs another.
What You’ll Learn
async/awaitsyntax and how coroutines work- The event loop: how Python manages concurrency
- Tasks, futures, and gathering results
aiohttpfor async HTTP requests- When async helps vs hurts (performance trade-offs)
Why Async Matters
Durga Antivirus Pro scans hundreds of files simultaneously — each file read is I/O-bound, making async a perfect fit. DodaZIP compresses multiple archives concurrently. A web scraper that fetches 100 URLs sequentially takes 100 seconds (1 second each). With async, it completes in ~2 seconds. Async is for I/O-bound concurrency, not CPU-bound parallelism.
flowchart LR
A["Generators"] --> B["Context Managers"]
B --> C["Async Programming"]
C --> D["Type Hints"]
D --> E["Testing"]
A:::done --> B:::done --> C:::current
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#2563eb,stroke:#2563eb,color:#fff
style C fill:#2563eb,stroke:#2563eb,color:#fff
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Coroutines and async/await
A coroutine is a function declared with async def. It can be paused with await and resumed later:
import asyncio
async def greet(name: str) -> str:
await asyncio.sleep(1) # Simulate I/O delay
return f"Hello, {name}!"
# Run the coroutine
result = asyncio.run(greet("Alice"))
print(result) # Hello, Alice! (after ~1 second)async defmakes a function a coroutineawaitpauses the coroutine until the awaited operation completesasyncio.run()starts the event loop and runs the top-level coroutine
The Event Loop
The event loop is the core of asyncio — it manages a queue of coroutines, running one until it hits an await, then switching to another:
async def task(name: str, delay: float):
print(f"{name}: start")
await asyncio.sleep(delay)
print(f"{name}: end after {delay}s")
return delay
async def main():
# Run tasks concurrently
results = await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 0.5),
)
print(f"All done: {results}")
asyncio.run(main())Output:
A: start
B: start
C: start
C: end after 0.5s
B: end after 1s
A: end after 2s
All done: [2, 1, 0.5]Total time: ~2 seconds (not 3.5 seconds) — tasks ran concurrently.
Tasks vs Coroutines
A Task wraps a coroutine and schedules it on the event loop. Use asyncio.create_task() for fire-and-forget concurrency:
async def slow_operation(n: int) -> int:
await asyncio.sleep(n)
return n * 2
async def main():
# Create tasks — they start running immediately
task1 = asyncio.create_task(slow_operation(2))
task2 = asyncio.create_task(slow_operation(1))
# Await results — may return in any order
print(await task2) # 2 (finishes first)
print(await task1) # 4
asyncio.run(main())Async HTTP with aiohttp
Install: pip install aiohttp
import asyncio
import aiohttp
async def fetch_url(session: aiohttp.ClientSession, url: str) -> dict:
async with session.get(url) as response:
return {"url": url, "status": response.status, "size": len(await response.text())}
async def fetch_all(urls: list[str]) -> list[dict]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
def main():
urls = [
"https://httpbin.org/html" for _ in range(5)
]
results = asyncio.run(fetch_all(urls))
for r in results:
print(f'{r["url"]}: status={r["status"]}, size={r["size"]}')
if __name__ == "__main__":
main()When Async Helps vs Hurts
| Scenario | Async | Sync |
|---|---|---|
| 100 web API calls | ✅ ~2s | ❌ ~100s |
| File I/O (100 files) | ✅ ~1s | ❌ ~50s |
| CPU-bound (image processing) | ❌ Same as sync | ✅ |
| Simple script (3 operations) | ❌ Overhead | ✅ ~3s |
Async excels at I/O-bound tasks — network requests, database queries, file reads where the program waits for external resources. Async never makes CPU-bound code faster.
Real-World Example: Async Web Scraper
import asyncio
import aiohttp
from typing import Optional
class AsyncScraper:
def __init__(self, max_concurrent: int = 10):
self.semaphore = asyncio.Semaphore(max_concurrent)
async def fetch(self, session: aiohttp.ClientSession, url: str) -> Optional[str]:
async with self.semaphore: # Limit concurrent connections
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(10)) as resp:
return await resp.text()
except Exception as e:
print(f"Failed {url}: {e}")
return None
async def scrape_many(self, urls: list[str]) -> list[Optional[str]]:
async with aiohttp.ClientSession() as session:
tasks = [self.fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
def run(self, urls: list[str]) -> list[Optional[str]]:
return asyncio.run(self.scrape_many(urls))
scraper = AsyncScraper(max_concurrent=5)
results = scraper.run([f"https://httpbin.org/delay/{i}" for i in range(1, 4)])
print(f"Fetched {len([r for r in results if r])} pages successfully")Common Mistakes
1. Using time.sleep() Instead of asyncio.sleep()
async def bad():
time.sleep(1) # Blocks the entire event loop!Fix: Use await asyncio.sleep(1) — it yields control back to the event loop.
2. Forgetting to await a Coroutine
result = my_coroutine() # Returns a coroutine object, not the result!Fix: result = await my_coroutine()
3. Running Blocking CPU Code in Coroutines
Long-running CPU tasks block the event loop. Offload them to a thread pool:
import asyncio
import time
async def main():
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, time.sleep, 2)
print("Done")4. Not Using asyncio.gather() for Concurrency
# Sequential — wrong
result1 = await fetch(url1)
result2 = await fetch(url2)
# Concurrent — correct
result1, result2 = await asyncio.gather(fetch(url1), fetch(url2))5. Creating Too Many Connections
Opening 1000 concurrent connections can overwhelm your network or the target server. Use asyncio.Semaphore to limit concurrency.
Practice Questions
1. What’s the difference between a coroutine and a regular function?
A coroutine is defined with async def and can be paused/resumed with await. Regular functions run to completion in one go.
2. Why is time.sleep() bad in async code?
It blocks the entire event loop thread, preventing other coroutines from running.
3. What does asyncio.gather() do?
Runs multiple coroutines concurrently and returns results in the same order as the input.
4. When should you NOT use async?
For CPU-bound tasks, simple scripts, or when the overhead of the event loop outweighs the concurrency benefit.
Challenge: Write a program that fetches 10 URLs concurrently and measures the total time vs sequential fetching.
Solution
import asyncio
import aiohttp
import time
async def fetch_one(session, url):
async with session.get(url) as resp:
return await resp.text()
async def fetch_all_concurrent(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)
urls = ["https://httpbin.org/delay/1" for _ in range(5)]
start = time.time()
asyncio.run(fetch_all_concurrent(urls))
print(f"Concurrent: {time.time() - start:.2f}s")Mini Project: Async File Downloader
import asyncio
import aiohttp
from pathlib import Path
async def download_file(session: aiohttp.ClientSession, url: str, dest: Path) -> bool:
try:
async with session.get(url) as response:
content = await response.read()
dest.write_bytes(content)
print(f"Downloaded {url} -> {dest.name}")
return True
except Exception as e:
print(f"Failed {url}: {e}")
return False
async def download_all(urls: list[str], dest_dir: str = "/tmp/downloads"):
Path(dest_dir).mkdir(parents=True, exist_ok=True)
async with aiohttp.ClientSession() as session:
tasks = []
for i, url in enumerate(urls):
dest = Path(dest_dir) / f"file_{i}.html"
tasks.append(download_file(session, url, dest))
return await asyncio.gather(*tasks)
urls = [
f"https://httpbin.org/html" for _ in range(3)
]
successes = asyncio.run(download_all(urls))
print(f"Successfully downloaded: {sum(successes)}/{len(urls)}")Expected output: Each file downloaded to /tmp/downloads/ concurrently.
What’s Next
Async programming completes your Python concurrency toolkit. Next, solidify your knowledge with type hints and testing.
| Topic | Description | Link |
|---|---|---|
| Python Type Hints | Static typing with mypy | https://tutorials.dodatech.com/programming-languages/python/py-type-hints/ |
| Python Testing | pytest and mocking | https://tutorials.dodatech.com/programming-languages/python/py-testing/ |
| Python Generators | Foundation of async | https://tutorials.dodatech.com/programming-languages/python/py-generators/ |
Practice tip: Add async scanning to your BankAccount project — simulate concurrent transaction processing with asyncio.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro