Lesson 12 · Python standard library deep dive
Python asyncio Explained: async/await & Event Loops | Standard Library #12
Video twelve of the twenty-five-part series: asyncio, Python's cooperative concurrency model for I/O-bound code. Coroutines, the event loop, gather, tasks,…
- CoursePython standard library deep dive
- Lesson12 of 24
- Video17 min
- FormatJupyter notebook · 11 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbPython Standard Library Deep-Dive, Video 12: asyncio#
- Video twelve of the twenty-five-part series: asyncio, Python's cooperative concurrency model for I/O-bound code.
- Coroutines, the event loop, gather, tasks, timeouts, and the Jupyter-specific setup this actually needs to run.
- Let's get into it.
Part 1: What asyncio Offers, and a Jupyter Setup Note#
import asyncio
import time
try:
import nest_asyncio
nest_asyncio.apply()
except ImportError:
pass
print('asyncio ready')
Part 2: async def and Coroutines#
async def greet(name):
return f'Hello, {name}!'
result = greet('Ana')
print(type(result))
print(result)
async def main():
result = await greet('Sam')
print(result)
asyncio.run(main())
Part 3: await and Sequential Execution#
async def fetch_data(name, delay):
await asyncio.sleep(delay)
return f'{name} fetched'
async def sequential_main():
start = time.time()
result1 = await fetch_data('A', 0.3)
result2 = await fetch_data('B', 0.3)
elapsed = time.time() - start
return result1, result2, elapsed
r1, r2, elapsed = asyncio.run(sequential_main())
print(r1, r2)
print(elapsed >= 0.6)
Part 4: asyncio.gather() - Running Coroutines Concurrently#
async def concurrent_main():
start = time.time()
result1, result2 = await asyncio.gather(
fetch_data('A', 0.3),
fetch_data('B', 0.3)
)
elapsed = time.time() - start
return result1, result2, elapsed
r1, r2, elapsed = asyncio.run(concurrent_main())
print(r1, r2)
print(elapsed < 0.6)
Part 5: Tasks with asyncio.create_task()#
async def task_main():
task1 = asyncio.create_task(fetch_data('A', 0.2))
task2 = asyncio.create_task(fetch_data('B', 0.2))
print('Both tasks are genuinely already running now')
result1 = await task1
result2 = await task2
return result1, result2
print(asyncio.run(task_main()))
Part 6: asyncio.sleep() vs time.sleep()#
async def blocking_mistake():
start = time.time()
await asyncio.gather(
blocking_task('A'),
blocking_task('B')
)
return time.time() - start
async def blocking_task(name):
time.sleep(0.2)
return name
elapsed = asyncio.run(blocking_mistake())
print(elapsed >= 0.4)
Part 7: Timeouts with asyncio.wait_for()#
async def slow_task():
await asyncio.sleep(2)
return 'finished'
async def timeout_main():
try:
result = await asyncio.wait_for(slow_task(), timeout=0.3)
return result
except asyncio.TimeoutError:
return 'genuinely timed out'
print(asyncio.run(timeout_main()))
Part 8: async with and asyncio.Lock#
shared_counter = 0
lock = asyncio.Lock()
async def safe_increment():
global shared_counter
async with lock:
current = shared_counter
await asyncio.sleep(0.01)
shared_counter = current + 1
async def lock_main():
await asyncio.gather(*(safe_increment() for _ in range(10)))
return shared_counter
print(asyncio.run(lock_main()))
Part 9: asyncio.Queue - Producer-Consumer#
async def producer(q, n):
for i in range(n):
await q.put(i)
await q.put(None)
async def consumer(q, results):
while True:
item = await q.get()
if item is None:
break
results.append(item * item)
async def queue_main():
q = asyncio.Queue()
results = []
await asyncio.gather(producer(q, 5), consumer(q, results))
return results
print(asyncio.run(queue_main()))
Part 10: Common Patterns#
async def fetch_user(user_id):
await asyncio.sleep(0.1)
return {'id': user_id, 'name': f'User{user_id}'}
async def fetch_all_users(user_ids):
tasks = [fetch_user(uid) for uid in user_ids]
return await asyncio.gather(*tasks)
start = time.time()
users = asyncio.run(fetch_all_users([1, 2, 3, 4, 5]))
elapsed = time.time() - start
print(len(users))
print(elapsed < 0.3)
Wrap-Up: What You Learned#
- asyncio runs many I/O-bound coroutines cooperatively on one thread; nest_asyncio fixes running it inside Jupyter's own loop.
- async def defines a coroutine function; calling it creates a coroutine object, only await actually runs it.
- asyncio.run starts the event loop and runs a top-level coroutine to completion.
- Sequential awaits run one at a time; gather runs several coroutines concurrently.
- create_task starts a coroutine running immediately in the background, to be awaited later.
- asyncio.sleep yields control back to the event loop; time.sleep blocks it entirely, breaking concurrency.
- wait_for adds a timeout to any coroutine, raising TimeoutError if it's exceeded.
- async with asyncio.Lock protects shared state across coroutines; asyncio.Queue coordinates producers and consumers.
- A real pattern: fetching many independent records concurrently instead of one at a time.
- That wraps up asyncio. Next up: logging, for genuinely production-grade diagnostics.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



