Getting Started with asyncio
Asynchronous programming lets your code handle multiple operations without waiting for each to finish. When you’re fetching data from APIs, reading files, or waiting for database queries, your program can work on other things instead of sitting idle. The asyncio module is Python’s built-in tool for this kind of work.
This guide covers the core concepts you need to start writing async code: coroutines, the event loop, and the async/await syntax.
Why Asyncio?
Traditional Python code runs synchronously—one line finishes before the next starts. If a line takes time (like waiting for a network response), everything stops:
import time
def fetch_data():
time.sleep(2) # Blocks for 2 seconds
return "data"
def main():
start = time.time()
result1 = fetch_data()
result2 = fetch_data()
print(f"Total time: {time.time() - start:.2f}s")
main()
# Output: Total time: 4.00s
With async code, both fetches happen “at the same time”:
import asyncio
import time
async def fetch_data():
await asyncio.sleep(2) # Non-blocking pause
return "data"
async def main():
start = time.time()
# Run both concurrently
result1, result2 = await asyncio.gather(
fetch_data(),
fetch_data()
)
print(f"Total time: {time.time() - start:.2f}s")
asyncio.run(main())
# Output: Total time: 2.00s
The key difference: asyncio.sleep() doesn’t block the entire program. While one coroutine waits, others can run.
Coroutines: async def Functions
A coroutine is a function defined with async def. Calling it doesn’t run the code—it returns a coroutine object that represents “work to be done”:
async def say_hello():
print("Hello")
# This doesn't print anything yet
coro = say_hello()
print(coro) # <coroutine object say_hello at ...>
To actually run the code inside a coroutine, you need to await it or pass it to the event loop.
The await Keyword
await pauses the current coroutine until the awaited coroutine finishes:
import asyncio
async def step_one():
await asyncio.sleep(1)
print("Step 1 done")
async def step_two():
await asyncio.sleep(1)
print("Step 2 done")
async def main():
await step_one()
await step_two()
asyncio.run(main())
# Output:
# Step 1 done
# Step 2 done
These run sequentially (2 seconds total). To run them concurrently, use asyncio.gather().
Running Coroutines Concurrently
asyncio.gather() runs multiple coroutines at the same time:
import asyncio
async def fetch(url):
await asyncio.sleep(1) # Simulate network request
return f"Data from {url}"
async def main():
results = await asyncio.gather(
fetch("api.example.com"),
fetch("api2.example.com"),
fetch("api3.example.com"),
)
print(results)
asyncio.run(main())
# Output: ['Data from api.example.com', 'Data from api2.example.com', 'Data from api3.example.com']
All three “requests” complete in 1 second instead of 3.
The Event Loop
The event loop is the engine that runs async code. It schedules coroutines and handles I/O operations. You rarely need to interact with it directly—asyncio.run() handles everything:
asyncio.run(main())
This creates a new event loop, runs main() until it completes, then closes the loop.
For more control, you can manage the loop yourself:
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
But for most cases, asyncio.run() is all you need.
Creating Tasks
A task schedules a coroutine to run on the event loop. Use asyncio.create_task() when you want to start something running without waiting for it:
import asyncio
async def background_task():
await asyncio.sleep(3)
print("Background task done!")
async def main():
task = asyncio.create_task(background_task())
print("Task created, not waiting...")
await asyncio.sleep(1)
print("Doing other work...")
await task # Now wait for it
print("All done")
asyncio.run(main())
# Output:
# Task created, not waiting...
# Doing other work...
# Background task done!
# All done
Tasks let you run background work while the main coroutine continues.
Waiting for Multiple Tasks
Sometimes you want to wait for several tasks but handle them as they complete:
import asyncio
async def task_with_duration(name, seconds):
await asyncio.sleep(seconds)
return f"{name} done"
async def main():
done, pending = await asyncio.wait(
[
asyncio.create_task(task_with_duration("fast", 1)),
asyncio.create_task(task_with_duration("medium", 2)),
asyncio.create_task(task_with_duration("slow", 3)),
],
return_when=asyncio.FIRST_COMPLETED
)
print(f"Completed: {[t.result() for t in done]}")
print(f"Still pending: {len(pending)}")
asyncio.run(main())
asyncio.wait() returns when the first task completes (or all complete, depending on return_when).
Async Context Managers
Just like regular context managers (with statements), async ones use __aenter__ and __aexit__:
import asyncio
class AsyncResource:
async def __aenter__(self):
await asyncio.sleep(0.1) # Simulate setup
print("Acquired resource")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await asyncio.sleep(0.1) # Simulate cleanup
print("Released resource")
async def main():
async with AsyncResource():
print("Using resource")
asyncio.run(main())
# Output:
# Acquired resource
# Using resource
# Released resource
Async Iterators and Generators
For streaming data, use async iterators:
import asyncio
class AsyncCounter:
def __init__(self, limit):
self.limit = limit
self.current = 0
def __aiter__(self):
return self
async def __anext__(self):
await asyncio.sleep(0.1)
if self.current >= self.limit:
raise StopAsyncIteration
self.current += 1
return self.current
async def main():
async for number in AsyncCounter(5):
print(number)
asyncio.run(main())
# Output: 1, 2, 3, 4, 5
Common Async Patterns
Running Sync Code in Async Context
If you have blocking synchronous code, run it in a thread pool:
import asyncio
from concurrent.futures import ThreadPoolExecutor
def blocking_io():
# This would block if it did real I/O
import time
time.sleep(1)
return "done"
async def main():
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, blocking_io)
print(result)
asyncio.run(main())
Timeout Handling
Set a timeout for operations:
import asyncio
async def slow_operation():
await asyncio.sleep(10)
return "finished"
async def main():
try:
result = await asyncio.wait_for(slow_operation(), timeout=2.0)
except asyncio.TimeoutError:
print("Operation timed out!")
asyncio.run(main())
# Output: Operation timed out!
Summary
Asyncio provides a powerful way to handle concurrent operations without threads. The core concepts are:
async defdefines a coroutineawaitpauses until another coroutine finishesasyncio.gather()runs multiple coroutines concurrentlyasyncio.run()is the entry point for async programs- Tasks schedule coroutines to run in the background
Start with these basics, then explore asyncio.StreamReader, queues, and synchronization primitives as needed.
See Also
asyncio— The asyncio module reference with complete API documentation- Async Programming with asyncio — A more comprehensive tutorial on asyncio patterns
concurrent.futures— Thread and process pools for parallel execution