Async Programming in Python
Asynchronous programming allows Python to handle I/O-bound operations efficiently. Let's dive deep into asyncio.
Understanding Coroutines
Coroutines are the building blocks of async Python:
import asyncio
async def fetch_data():
print("Fetching...")
await asyncio.sleep(2) # Simulates I/O
print("Done!")
return {"data": "result"}
# Running a coroutine
asyncio.run(fetch_data())
Running Multiple Tasks
Execute tasks concurrently with asyncio.gather:
async def fetch_user(user_id):
await asyncio.sleep(1)
return f"User {user_id}"
async def main():
users = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3)
)
print(users) # Completes in ~1 second, not 3
asyncio.run(main())
Creating Tasks
For more control, create tasks explicitly:
async def main():
task1 = asyncio.create_task(fetch_data())
task2 = asyncio.create_task(fetch_data())
result1 = await task1
result2 = await task2
Async Context Managers
Handle resources properly with async context managers:
import aiohttp
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
Error Handling
Handle exceptions in async code:
async def safe_fetch(url):
try:
return await fetch_url(url)
except aiohttp.ClientError as e:
print(f"Error fetching {url}: {e}")
return None
Async Generators
Yield values asynchronously:
async def async_range(start, stop):
for i in range(start, stop):
await asyncio.sleep(0.1)
yield i
async def main():
async for num in async_range(0, 5):
print(num)
Best Practices
- Don't block the event loop - Avoid CPU-heavy operations
- Use asyncio.gather for parallel I/O - It's faster than sequential awaits
- Handle timeouts - Use
asyncio.wait_forfor time limits - Clean up resources - Use async context managers
Conclusion
Async programming is essential for modern Python applications. Master these patterns to build fast, scalable applications.