Python Asynchronous Programming (asyncio)
Synchronous vs Asynchronous
| Synchronous | Asynchronous | |
|---|---|---|
| Execution | Sequential, line by line | Can pause and resume; event-driven |
| Blocking | Yes — must wait for each operation | No — can do other work while waiting |
| Threading needed | No | No — runs in one thread |
| Complexity | Simple | Simpler than threading |
Key Terminology
| Term | Definition |
|---|---|
| Coroutine | An async function — can pause execution and give control to other coroutines |
await |
Pauses the current coroutine and waits for another coroutine/task to finish |
| Task | A scheduled coroutine — runs concurrently without blocking |
| Event loop | Manages scheduling and switching between coroutines/tasks |
| Future | An object that will have a value at some point — tasks are a type of future |
asyncio.gather() |
Schedules multiple coroutines as tasks and runs them concurrently |
Setup
import asyncio
Defining a Coroutine
Add async before def:
async def main():
print("main")
print(type(main)) # <class 'function'>
print(type(main())) # <class 'coroutine'> ← calling it returns a coroutine, not a result
Running Async Code — Entry Point
You must always start your async program with asyncio.run():
asyncio.run(main()) # sets up the event loop and runs the main coroutine
This is always required — you cannot
awaitoutside of a coroutine.
await — Calling a Coroutine
Use await inside a coroutine to call another coroutine and wait for it to finish:
async def print_something():
await asyncio.sleep(1) # use asyncio.sleep, NOT time.sleep
print("something")
return "finished"
async def main():
result = await print_something() # blocks until print_something() is done
print(result) # "finished"
asyncio.run(main())
⚠️ Always use
asyncio.sleep()in async code —time.sleep()blocks the entire event loop.
Tasks — Running Concurrently
Wrapping a coroutine in a task schedules it to run concurrently — it doesn't block:
async def main():
task = asyncio.create_task(print_something()) # scheduled, starts soon
print("main continues immediately")
await task # wait for task to finish
result = await task # get return value
- If the program ends before a task finishes → the task is cancelled
- Always
awaita task if you need it to complete
await vs create_task — The Core Difference
# Blocking — waits for print_something to finish before continuing
await print_something()
print("this runs after")
# Non-blocking — schedules it; continues immediately
task = asyncio.create_task(print_something())
print("this runs right away")
await task # wait later when needed
Practical Example — Concurrent Tasks
async def print_values(values, delay):
for item in values:
print(item)
await asyncio.sleep(delay)
return delay
async def main():
task1 = asyncio.create_task(print_values([1, 3, 5], 0.2))
task2 = asyncio.create_task(print_values([2, 4], 0.3))
await task1
await task2
asyncio.run(main())
# Output: 1 2 3 4 5 (in order — reliable, no locks needed!)
asyncio.gather() — Shortcut for Running Multiple Tasks
Schedules all coroutines as tasks, runs them concurrently, and returns a list of all return values:
async def main():
results = await asyncio.gather(
print_values([1, 3, 5], 0.2),
print_values([2, 4], 0.3)
)
print(results) # [0.2, 0.3] — return values in order
This is equivalent to manually creating tasks and awaiting them — just more concise.
Real-World Use Case — Fetch Data + Run Algorithm
async def fetch_data():
print("Start fetching")
await asyncio.sleep(2) # simulates network delay
print("Done fetching")
return [1, 2, 3]
async def run_algorithm():
for i in range(10):
print(i)
await asyncio.sleep(0.5)
async def main():
# Run both concurrently — don't wait for fetch before starting algorithm
data, _ = await asyncio.gather(fetch_data(), run_algorithm())
print(data)
Async Generators
A generator function with async and yield:
async def gen(n):
for i in range(n):
yield i
await asyncio.sleep(0.5)
Iterating over an async generator
Must use async for — regular for will raise an error:
async def main():
async for i in gen(10): # ✅ async for
print(i)
for i in gen(10): # ❌ TypeError — not iterable
print(i)
Async Methods in Classes
class MyClass:
@staticmethod
async def test():
print("hi")
async def main():
await MyClass.test()
Async vs Threading — Why Prefer Async?
| Threading | Asyncio | |
|---|---|---|
| Complexity | High (locks, deadlocks, race conditions) | Low (cleaner syntax) |
| Runs in | Multiple threads | One thread |
| True parallelism | ❌ (blocked by GIL) | ❌ (but same effect for I/O) |
| Concurrency | ✅ | ✅ |
| Preferred for I/O tasks | Sometimes | ✅ Yes |
Since the GIL prevents true parallelism in Python anyway, asyncio achieves the same concurrency benefits as threading but with far less complexity.
Key Takeaways & Recap
| Concept | Summary |
|---|---|
async def |
Defines a coroutine — returns a coroutine object when called |
await |
Pauses current coroutine; waits for another coroutine/task |
asyncio.run() |
Entry point — always required to start async code |
asyncio.create_task() |
Schedules a coroutine to run concurrently (non-blocking) |
asyncio.gather() |
Runs multiple coroutines concurrently; returns list of results |
asyncio.sleep() |
Use instead of time.sleep() in async code |
async for |
Required to iterate over async generators/iterables |
| Task/Future | An object representing a value that will exist in the future |
| Prefer async over threads | Simpler, cleaner, avoids GIL-related threading headaches |