Ever felt your Python script freeze while waiting for a network call or file read? That’s blocking code, and it kills performance. Asyncio is Python’s built‑in solution that lets you run many tasks at once without threads. The result? Faster, more responsive programs that still look clean.
First, you avoid the headache of multithreading—no race conditions, no locks. Asyncio works on a single thread, switching tasks only when they’re waiting for I/O. That means lower memory use and easier debugging. Second, modern services (APIs, websockets, databases) are all I/O‑heavy, so async code can handle many connections with just a few lines.
In real life, think of a web scraper that pulls data from 100 sites. With traditional loops, each request blocks the next one, making the job take minutes. With asyncio, all 100 requests are launched together, and the program moves on as soon as any response arrives. The speed boost can be dramatic.
Start by importing the module and defining an async def
function. Inside, use await
before any call that returns an awaitable—like asyncio.sleep()
or an HTTP request from aiohttp
. Here’s a tiny example:
import asyncio
async def hello():
print('Start')
await asyncio.sleep(1)
print('Done')
asyncio.run(hello())
The asyncio.run()
call creates an event loop, runs the coroutine, and shuts the loop down cleanly. This pattern replaces the old get_event_loop()
boilerplate and works in most scripts.
When you need to run several coroutines together, wrap them with asyncio.gather()
:
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
urls = ['https://example.com', 'https://python.org']
results = asyncio.run(asyncio.gather(*(fetch(u) for u in urls)))
Notice how the list comprehension builds a bunch of coroutines, and gather
schedules them all at once. The event loop flips between them whenever one awaits I/O, keeping the CPU busy.
Beware of common pitfalls: never block the loop with a regular time.sleep()
or heavy computation. If you must run CPU‑bound work, offload it to a thread pool with loop.run_in_executor()
. Also, remember that not every library is async‑ready; mixing sync and async code can reintroduce blocking.
Debugging async code is easier than you think. Use asyncio.Task.all_tasks()
(or asyncio.all_tasks()
in Python 3.7+) to inspect pending tasks, and watch for “Task was destroyed but it is pending!” warnings—they signal missed awaits.
If you’re building a web service, check out FastAPI
. It’s built on top of Starlette and fully embraces asyncio, letting you write endpoint functions with async def
and get high‑performance, non‑blocking I/O for free.
In short, asyncio lets you write code that feels synchronous while running many operations in the background. Start small, replace a few blocking calls with async equivalents, and watch your app become snappier. Happy coding!
Real-world Python tricks for 2025: idioms, performance wins, typing, async, tooling, and packaging. Concrete examples, checklists, and a battle-tested workflow.