Asynchronous Programming
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand async/await syntax
- - Use the asyncio module
- - Create and work with coroutines
- - Understand event loops
- - Handle asynchronous I/O operations
- - Use async context managers
- - Work with async generators
- - Apply async patterns in real-world scenarios
- - Debug async code
- - Understand when to use async programming
Lesson 16.3: Asynchronous Programming
Learning Objectives
By the end of this lesson, you will be able to:
- Understand async/await syntax
- Use the asyncio module
- Create and work with coroutines
- Understand event loops
- Handle asynchronous I/O operations
- Use async context managers
- Work with async generators
- Apply async patterns in real-world scenarios
- Debug async code
- Understand when to use async programming
Introduction to Asynchronous Programming
Asynchronous programming allows you to write concurrent code that can handle multiple operations without blocking. Python's asyncio module provides support for asynchronous programming.
Why Asynchronous Programming?
- Non-blocking I/O: Handle I/O operations without blocking
- Concurrency: Run multiple tasks concurrently
- Efficiency: Better resource utilization
- Scalability: Handle many connections efficiently
- Modern approach: Foundation for modern Python applications
What is Asynchronous Programming?
Asynchronous programming allows code to start a task, move on to other tasks while waiting, and come back to the original task when it's ready.
async/await Syntax
Basic async Function
An async function is a coroutine function that returns a coroutine object:
import asyncio
async def hello():
print("Hello")
await asyncio.sleep(1)
print("World")
# Run the coroutine
asyncio.run(hello())
Understanding async
The async keyword defines a coroutine function:
async def my_function():
return "Result"
# Calling async function returns a coroutine
coro = my_function()
print(type(coro)) # <class 'coroutine'>
# Must await or run with asyncio
result = asyncio.run(coro)
print(result) # Result
Understanding await
The await keyword pauses execution until the awaited coroutine completes:
import asyncio
async def fetch_data():
print("Fetching data...")
await asyncio.sleep(2) # Simulate I/O
return "Data"
async def main():
result = await fetch_data()
print(f"Got: {result}")
asyncio.run(main())
Multiple await Calls
import asyncio
async def task1():
await asyncio.sleep(1)
return "Task 1"
async def task2():
await asyncio.sleep(1)
return "Task 2"
async def main():
result1 = await task1()
result2 = await task2()
print(f"{result1}, {result2}")
asyncio.run(main()) # Takes 2 seconds (sequential)
Concurrent Execution
import asyncio
async def task1():
await asyncio.sleep(1)
return "Task 1"
async def task2():
await asyncio.sleep(1)
return "Task 2"
async def main():
# Run concurrently
result1, result2 = await asyncio.gather(task1(), task2())
print(f"{result1}, {result2}")
asyncio.run(main()) # Takes 1 second (concurrent)
asyncio Module
Running Coroutines
The asyncio.run() function runs a coroutine:
import asyncio
async def main():
print("Hello")
await asyncio.sleep(1)
print("World")
asyncio.run(main())
Creating Tasks
Tasks schedule coroutines to run concurrently:
import asyncio
async def worker(name, delay):
print(f"{name} starting")
await asyncio.sleep(delay)
print(f"{name} finished")
return f"{name} result"
async def main():
# Create tasks
task1 = asyncio.create_task(worker("Worker1", 1))
task2 = asyncio.create_task(worker("Worker2", 2))
# Wait for tasks
result1 = await task1
result2 = await task2
print(f"Results: {result1}, {result2}")
asyncio.run(main())
asyncio.gather()
gather() runs multiple coroutines concurrently:
import asyncio
async def task(name, delay):
await asyncio.sleep(delay)
return f"{name} completed"
async def main():
results = await asyncio.gather(
task("Task1", 1),
task("Task2", 2),
task("Task3", 1)
)
print(results) # ['Task1 completed', 'Task2 completed', 'Task3 completed']
asyncio.run(main()) # Takes ~2 seconds (longest task)
asyncio.wait()
wait() waits for tasks with different completion strategies:
import asyncio
async def task(name, delay):
await asyncio.sleep(delay)
return f"{name} done"
async def main():
tasks = [
asyncio.create_task(task("Task1", 1)),
asyncio.create_task(task("Task2", 2)),
asyncio.create_task(task("Task3", 3))
]
# Wait for first completed
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
print(f"Completed: {[t.result() for t in done]}")
# Wait for remaining
done, pending = await asyncio.wait(pending)
print(f"All completed: {[t.result() for t in done]}")
asyncio.run(main())
asyncio.sleep()
asyncio.sleep() is a non-blocking sleep:
import asyncio
async def main():
print("Start")
await asyncio.sleep(1) # Non-blocking
print("End")
asyncio.run(main())
Coroutines
What are Coroutines?
Coroutines are functions that can be paused and resumed. They're created with async def:
import asyncio
async def coroutine():
print("Coroutine started")
await asyncio.sleep(1)
print("Coroutine finished")
return "Result"
# Coroutine object
coro = coroutine()
print(type(coro)) # <class 'coroutine'>
# Run coroutine
result = asyncio.run(coro)
print(result) # Result
Coroutine Execution
import asyncio
async def coroutine():
print("Step 1")
await asyncio.sleep(0.5)
print("Step 2")
await asyncio.sleep(0.5)
print("Step 3")
return "Done"
asyncio.run(coroutine())
Chaining Coroutines
import asyncio
async def step1():
await asyncio.sleep(1)
return "Step 1"
async def step2(data):
await asyncio.sleep(1)
return f"{data} -> Step 2"
async def step3(data):
await asyncio.sleep(1)
return f"{data} -> Step 3"
async def main():
result1 = await step1()
result2 = await step2(result1)
result3 = await step3(result2)
print(result3)
asyncio.run(main())
Coroutine with Exception Handling
import asyncio
async def might_fail():
await asyncio.sleep(1)
raise ValueError("Error occurred")
async def main():
try:
await might_fail()
except ValueError as e:
print(f"Caught: {e}")
asyncio.run(main())
Event Loops
Understanding Event Loops
An event loop manages and distributes the execution of different tasks:
import asyncio
async def task(name):
print(f"{name} starting")
await asyncio.sleep(1)
print(f"{name} finished")
# Get event loop
loop = asyncio.get_event_loop()
# Run coroutine
loop.run_until_complete(task("Task1"))
# Close loop
loop.close()
Using asyncio.run()
asyncio.run() creates an event loop, runs the coroutine, and closes the loop:
import asyncio
async def main():
print("Running in event loop")
asyncio.run(main())
Manual Event Loop Management
import asyncio
async def task():
print("Task running")
await asyncio.sleep(1)
# Create event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(task())
finally:
loop.close()
Scheduling Tasks
import asyncio
async def task(name):
print(f"{name} executed")
return f"{name} result"
async def main():
# Schedule tasks
task1 = asyncio.create_task(task("Task1"))
task2 = asyncio.create_task(task("Task2"))
# Wait for both
results = await asyncio.gather(task1, task2)
print(results)
asyncio.run(main())
Asynchronous I/O Operations
Async File I/O
import asyncio
async def read_file(filename):
with open(filename, 'r') as f:
content = f.read()
return content
async def main():
content = await read_file('data.txt')
print(content)
asyncio.run(main())
Async HTTP Requests
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch_url(session, 'https://example.com')
print(len(html))
asyncio.run(main())
Multiple HTTP Requests
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://python.org',
'https://github.com'
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"Fetched {len(results)} pages")
asyncio.run(main())
Async Context Managers
Creating Async Context Managers
import asyncio
class AsyncContextManager:
async def __aenter__(self):
print("Entering context")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Exiting context")
return False
async def main():
async with AsyncContextManager() as cm:
print("Inside context")
asyncio.run(main())
Using @asynccontextmanager
from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def async_context():
print("Entering")
try:
yield "resource"
finally:
print("Exiting")
async def main():
async with async_context() as resource:
print(f"Using {resource}")
asyncio.run(main())
Async Generators
Creating Async Generators
import asyncio
async def async_generator():
for i in range(5):
await asyncio.sleep(0.5)
yield i
async def main():
async for value in async_generator():
print(value)
asyncio.run(main())
Using Async Generators
import asyncio
async def countdown(n):
while n > 0:
await asyncio.sleep(1)
yield n
n -= 1
async def main():
async for num in countdown(5):
print(num)
asyncio.run(main())
Practical Examples
Example 1: Concurrent Downloads
import asyncio
import aiohttp
async def download_file(session, url, filename):
async with session.get(url) as response:
content = await response.read()
with open(filename, 'wb') as f:
f.write(content)
print(f"Downloaded {filename}")
async def main():
urls = [
("https://example.com/file1.txt", "file1.txt"),
("https://example.com/file2.txt", "file2.txt"),
("https://example.com/file3.txt", "file3.txt"),
]
async with aiohttp.ClientSession() as session:
tasks = [
download_file(session, url, filename)
for url, filename in urls
]
await asyncio.gather(*tasks)
asyncio.run(main())
Example 2: Web Scraper
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def fetch_page(session, url):
async with session.get(url) as response:
return await response.text()
async def parse_page(session, url):
html = await fetch_page(session, url)
soup = BeautifulSoup(html, 'html.parser')
return soup.title.string if soup.title else "No title"
async def main():
urls = [
'https://example.com',
'https://python.org',
'https://github.com'
]
async with aiohttp.ClientSession() as session:
tasks = [parse_page(session, url) for url in urls]
titles = await asyncio.gather(*tasks)
for url, title in zip(urls, titles):
print(f"{url}: {title}")
asyncio.run(main())
Example 3: Rate-Limited Requests
import asyncio
class RateLimiter:
def __init__(self, rate):
self.rate = rate
self.tokens = rate
self.updated_at = asyncio.get_event_loop().time()
async def acquire(self):
while self.tokens < 1:
await asyncio.sleep(0.1)
self._add_tokens()
self.tokens -= 1
def _add_tokens(self):
now = asyncio.get_event_loop().time()
elapsed = now - self.updated_at
self.tokens = min(self.rate, self.tokens + elapsed * self.rate)
self.updated_at = now
async def make_request(limiter, request_id):
await limiter.acquire()
print(f"Request {request_id} made")
await asyncio.sleep(0.1)
async def main():
limiter = RateLimiter(rate=2) # 2 requests per second
tasks = [make_request(limiter, i) for i in range(10)]
await asyncio.gather(*tasks)
asyncio.run(main())
Common Mistakes and Pitfalls
1. Forgetting await
# WRONG: Missing await
async def fetch_data():
return "Data"
async def main():
result = fetch_data() # Returns coroutine, not result!
print(result) # <coroutine object fetch_data at 0x...>
# CORRECT: Use await
async def main():
result = await fetch_data()
print(result) # Data
2. Blocking Operations in Async Code
# WRONG: Blocking operation
import time
async def bad_function():
time.sleep(1) # Blocks event loop!
# CORRECT: Use async sleep
async def good_function():
await asyncio.sleep(1) # Non-blocking
3. Not Using asyncio.run()
# WRONG: Calling coroutine directly
async def main():
print("Hello")
main() # Returns coroutine, doesn't run
# CORRECT: Use asyncio.run()
asyncio.run(main())
4. Mixing Sync and Async
# WRONG: Calling async from sync
def sync_function():
result = await async_function() # SyntaxError!
# CORRECT: Make function async
async def sync_function():
result = await async_function()
Best Practices
1. Always Use await for Async Operations
async def fetch_data():
await asyncio.sleep(1)
return "Data"
2. Use asyncio.gather() for Concurrent Operations
results = await asyncio.gather(task1(), task2(), task3())
3. Use Async Context Managers
async with async_context() as resource:
# Use resource
pass
4. Avoid Blocking Operations
# Use asyncio.sleep() instead of time.sleep()
await asyncio.sleep(1)
5. Handle Exceptions Properly
async def main():
try:
result = await might_fail()
except Exception as e:
print(f"Error: {e}")
Practice Exercise
Exercise: Async Programming
Objective: Create a Python program that demonstrates asynchronous programming.
Instructions:
-
Create a file called
async_practice.py -
Write a program that:
- Uses async/await syntax
- Creates and runs coroutines
- Uses asyncio module features
- Demonstrates concurrent execution
- Shows practical applications
-
Your program should include:
- Basic async functions
- Multiple coroutines
- Concurrent execution with gather()
- Tasks and event loops
- Real-world examples
Example Solution:
"""
Asynchronous Programming Practice
This program demonstrates async/await and asyncio.
"""
import asyncio
import time
print("=" * 60)
print("ASYNCHRONOUS PROGRAMMING PRACTICE")
print("=" * 60)
print()
# 1. Basic async function
print("1. BASIC ASYNC FUNCTION")
print("-" * 60)
async def hello():
print("Hello")
await asyncio.sleep(0.5)
print("World")
asyncio.run(hello())
print()
# 2. Multiple await calls (sequential)
print("2. MULTIPLE AWAIT CALLS (SEQUENTIAL)")
print("-" * 60)
async def task(name, delay):
print(f"{name} starting")
await asyncio.sleep(delay)
print(f"{name} finished")
return f"{name} result"
async def main():
start = time.time()
result1 = await task("Task1", 0.5)
result2 = await task("Task2", 0.5)
elapsed = time.time() - start
print(f"Results: {result1}, {result2}")
print(f"Time: {elapsed:.2f}s")
asyncio.run(main())
print()
# 3. Concurrent execution with gather()
print("3. CONCURRENT EXECUTION WITH gather()")
print("-" * 60)
async def main():
start = time.time()
results = await asyncio.gather(
task("Task1", 0.5),
task("Task2", 0.5),
task("Task3", 0.5)
)
elapsed = time.time() - start
print(f"Results: {results}")
print(f"Time: {elapsed:.2f}s (concurrent)")
asyncio.run(main())
print()
# 4. Creating tasks
print("4. CREATING TASKS")
print("-" * 60)
async def main():
task1 = asyncio.create_task(task("Task1", 1))
task2 = asyncio.create_task(task("Task2", 1))
result1 = await task1
result2 = await task2
print(f"Results: {result1}, {result2}")
asyncio.run(main())
print()
# 5. asyncio.wait()
print("5. asyncio.wait()")
print("-" * 60)
async def main():
tasks = [
asyncio.create_task(task("Task1", 0.5)),
asyncio.create_task(task("Task2", 1)),
asyncio.create_task(task("Task3", 0.3))
]
done, pending = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
print(f"All tasks completed: {[t.result() for t in done]}")
asyncio.run(main())
print()
# 6. Chaining coroutines
print("6. CHAINING COROUTINES")
print("-" * 60)
async def step1():
await asyncio.sleep(0.3)
return "Step 1"
async def step2(data):
await asyncio.sleep(0.3)
return f"{data} -> Step 2"
async def step3(data):
await asyncio.sleep(0.3)
return f"{data} -> Step 3"
async def main():
result1 = await step1()
result2 = await step2(result1)
result3 = await step3(result2)
print(result3)
asyncio.run(main())
print()
# 7. Exception handling
print("7. EXCEPTION HANDLING")
print("-" * 60)
async def might_fail(should_fail):
await asyncio.sleep(0.2)
if should_fail:
raise ValueError("Error occurred")
return "Success"
async def main():
try:
result = await might_fail(False)
print(f"Result: {result}")
except ValueError as e:
print(f"Caught: {e}")
try:
await might_fail(True)
except ValueError as e:
print(f"Caught: {e}")
asyncio.run(main())
print()
# 8. Async generator
print("8. ASYNC GENERATOR")
print("-" * 60)
async def async_countdown(n):
while n > 0:
await asyncio.sleep(0.2)
yield n
n -= 1
async def main():
async for num in async_countdown(5):
print(f"Countdown: {num}")
asyncio.run(main())
print()
# 9. Async context manager
print("9. ASYNC CONTEXT MANAGER")
print("-" * 60)
class AsyncContext:
async def __aenter__(self):
print("Entering context")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Exiting context")
return False
async def main():
async with AsyncContext() as ctx:
print("Inside context")
asyncio.run(main())
print()
# 10. Real-world: Concurrent tasks
print("10. REAL-WORLD: CONCURRENT TASKS")
print("-" * 60)
async def fetch_data(name, delay):
print(f"Fetching {name}...")
await asyncio.sleep(delay)
return f"{name} data"
async def main():
start = time.time()
results = await asyncio.gather(
fetch_data("User", 0.5),
fetch_data("Posts", 0.5),
fetch_data("Comments", 0.5)
)
elapsed = time.time() - start
print(f"Results: {results}")
print(f"Time: {elapsed:.2f}s (concurrent)")
asyncio.run(main())
print()
print("=" * 60)
print("PRACTICE COMPLETE!")
print("=" * 60)
Expected Output (truncated):
============================================================
ASYNCHRONOUS PROGRAMMING PRACTICE
============================================================
1. BASIC ASYNC FUNCTION
------------------------------------------------------------
Hello
World
[... rest of output ...]
Challenge (Optional):
- Create an async web scraper that fetches multiple pages concurrently
- Build an async file processor that processes multiple files in parallel
- Implement an async chat server using asyncio
- Create an async task queue system
Key Takeaways
- async/await - syntax for asynchronous programming
- asyncio module - provides async functionality
- Coroutines - functions that can be paused and resumed
- Event loops - manage async task execution
- asyncio.run() - runs coroutines
- asyncio.gather() - runs multiple coroutines concurrently
- Tasks - scheduled coroutines
- Async I/O - non-blocking I/O operations
- Async context managers -
async withstatement - Async generators -
async forstatement - Concurrency - multiple tasks running concurrently
- Non-blocking - doesn't block the event loop
- Best practices - always await, use gather(), avoid blocking
- When to use - I/O-bound operations, concurrent tasks
- Common mistakes - forgetting await, blocking operations
Quiz: Async
Test your understanding with these questions:
-
What does
async defcreate?- A) A regular function
- B) A coroutine function
- C) A generator
- D) A class
-
What does
awaitdo?- A) Waits for a coroutine to complete
- B) Creates a coroutine
- C) Runs a function
- D) Nothing
-
What is asyncio.run() used for?
- A) Creating tasks
- B) Running coroutines
- C) Creating event loops
- D) All of the above
-
What does asyncio.gather() do?
- A) Runs coroutines sequentially
- B) Runs coroutines concurrently
- C) Creates tasks
- D) Waits for tasks
-
What is a coroutine?
- A) A function
- B) A function that can be paused and resumed
- C) A generator
- D) A class
-
What is an event loop?
- A) A loop that runs forever
- B) Manages async task execution
- C) A for loop
- D) Nothing
-
What happens if you forget await?
- A) Nothing
- B) Gets coroutine object instead of result
- C) Error occurs
- D) Function runs normally
-
What should you use instead of time.sleep() in async code?
- A) time.sleep()
- B) asyncio.sleep()
- C) await sleep()
- D) sleep()
-
What is async with used for?
- A) Async for loops
- B) Async context managers
- C) Async functions
- D) Nothing
-
When should you use async programming?
- A) Always
- B) For I/O-bound operations
- C) For CPU-bound operations
- D) Never
Answers:
- B) A coroutine function (async def creates coroutine function)
- A) Waits for a coroutine to complete (await purpose)
- B) Running coroutines (asyncio.run() purpose)
- B) Runs coroutines concurrently (asyncio.gather() purpose)
- B) A function that can be paused and resumed (coroutine definition)
- B) Manages async task execution (event loop purpose)
- B) Gets coroutine object instead of result (forgetting await consequence)
- B) asyncio.sleep() (non-blocking sleep)
- B) Async context managers (async with purpose)
- B) For I/O-bound operations (when to use async)
Next Steps
Excellent work! You've mastered asynchronous programming. You now understand:
- async/await syntax
- asyncio module
- Coroutines
- Event loops
What's Next?
- Module 17: Testing
- Learn testing basics
- Understand unit testing
- Explore testing frameworks
Additional Resources
- asyncio: docs.python.org/3/library/asyncio.html
- PEP 492: peps.python.org/pep-0492/ (Coroutines with async and await syntax)
- PEP 525: peps.python.org/pep-0525/ (Asynchronous Generators)
Lesson completed! You're ready to move on to the next module.
Course Navigation
- Threading
- Multiprocessing
- Asynchronous Programming