Your First Async Program
Let’s write your first async program and see the magic of concurrent execution in action.
Basic Async Function
Start with a simple async function:
import asyncio
async def say_hello(name, delay):
"""Async function that greets someone after a delay"""
print(f"Hello {name}, starting...")
await asyncio.sleep(delay) # Non-blocking sleep
print(f"Hello {name}, finished after {delay}s!")
return f"Greeted {name}"
# Run a single async function
async def main():
result = await say_hello("Alice", 2)
print(f"Result: {result}")
# Execute the async program
asyncio.run(main())
Key points:
async def
creates an async function (coroutine)await
pauses execution until the operation completesasyncio.run()
starts the event loop and runs the main coroutine
Concurrent Execution
Now let’s see the real power - running multiple operations simultaneously:
import asyncio
import time
async def fetch_data(source, delay):
"""Simulate fetching data from different sources"""
print(f"Fetching from {source}...")
await asyncio.sleep(delay) # Simulate network delay
print(f"Got data from {source}")
return f"Data from {source}"
async def sequential_example():
"""Sequential execution - slow"""
print("=== Sequential Execution ===")
start_time = time.time()
result1 = await fetch_data("Database", 2)
result2 = await fetch_data("API", 1.5)
result3 = await fetch_data("Cache", 0.5)
total_time = time.time() - start_time
print(f"Sequential total time: {total_time:.1f}s\n")
return [result1, result2, result3]
async def concurrent_example():
"""Concurrent execution - fast"""
print("=== Concurrent Execution ===")
start_time = time.time()
# Start all operations simultaneously
task1 = asyncio.create_task(fetch_data("Database", 2))
task2 = asyncio.create_task(fetch_data("API", 1.5))
task3 = asyncio.create_task(fetch_data("Cache", 0.5))
# Wait for all to complete
results = await asyncio.gather(task1, task2, task3)
total_time = time.time() - start_time
print(f"Concurrent total time: {total_time:.1f}s\n")
return results
async def main():
# Run both examples
await sequential_example()
await concurrent_example()
asyncio.run(main())
Output shows the difference:
- Sequential: ~4 seconds (2 + 1.5 + 0.5)
- Concurrent: ~2 seconds (longest operation determines total time)
Common Patterns
Pattern 1: Fire and Forget
async def background_task(name):
"""Task that runs in background"""
await asyncio.sleep(3)
print(f"Background task {name} completed")
async def fire_and_forget_example():
"""Start tasks without waiting for them"""
# Start background tasks
asyncio.create_task(background_task("Task-1"))
asyncio.create_task(background_task("Task-2"))
# Do other work immediately
print("Main work starting...")
await asyncio.sleep(1)
print("Main work finished")
# Wait a bit to see background tasks complete
await asyncio.sleep(3)
asyncio.run(fire_and_forget_example())
Pattern 2: Timeout Handling
async def slow_operation():
"""Operation that might take too long"""
await asyncio.sleep(5)
return "Slow result"
async def timeout_example():
"""Handle operations with timeout"""
try:
# Wait maximum 2 seconds
result = await asyncio.wait_for(slow_operation(), timeout=2.0)
print(f"Got result: {result}")
except asyncio.TimeoutError:
print("Operation timed out!")
### Pattern 3: Error Handling in Concurrent Operations
```python
async def risky_operation(name, should_fail=False):
"""Operation that might fail"""
await asyncio.sleep(1)
if should_fail:
raise ValueError(f"Operation {name} failed!")
return f"Success from {name}"
async def error_handling_example():
"""Handle errors in concurrent operations"""
tasks = [
asyncio.create_task(risky_operation("Task-1", False)),
asyncio.create_task(risky_operation("Task-2", True)), # This will fail
asyncio.create_task(risky_operation("Task-3", False))
]
# Use return_exceptions=True to get both results and exceptions
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Task {i+1} failed: {result}")
else:
print(f"Task {i+1} succeeded: {result}")
asyncio.run(error_handling_example())
Real-World Example: Web Scraper
Here’s a practical example that demonstrates async power:
import asyncio
import aiohttp
import time
async def fetch_url(session, url):
"""Fetch a single URL"""
try:
async with session.get(url) as response:
content = await response.text()
return f"Success {url}: {len(content)} characters"
except Exception as e:
return f"Failed {url}: Error - {str(e)}"
async def scrape_websites():
"""Scrape multiple websites concurrently"""
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
"https://httpbin.org/status/200",
"https://httpbin.org/json"
]
start_time = time.time()
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
total_time = time.time() - start_time
print("Scraping Results:")
for result in results:
print(f" {result}")
print(f"\nTotal time: {total_time:.1f}s")
print(f"Average per URL: {total_time/len(urls):.1f}s")
# Run the scraper
asyncio.run(scrape_websites())
Common Mistakes and Solutions
Mistake 1: Forgetting await
# Wrong - creates coroutine but doesn't execute it
async def wrong_way():
result = fetch_data("API", 1) # Missing await!
print(result) # Prints: <coroutine object>
# Correct - actually executes the coroutine
async def right_way():
result = await fetch_data("API", 1) # With await
print(result) # Prints: "Data from API"
Mistake 2: Using time.sleep()
instead of asyncio.sleep()
# Wrong - blocks the entire event loop
async def blocking_sleep():
time.sleep(2) # Blocks everything!
# Correct - allows other tasks to run
async def non_blocking_sleep():
await asyncio.sleep(2) # Other tasks can run
Mistake 3: Not handling exceptions in concurrent tasks
# Wrong - one failure stops everything
async def fragile_approach():
results = await asyncio.gather(
risky_operation("A"),
risky_operation("B", should_fail=True), # This stops everything
risky_operation("C")
)
# Correct - handle exceptions gracefully
async def robust_approach():
results = await asyncio.gather(
risky_operation("A"),
risky_operation("B", should_fail=True),
risky_operation("C"),
return_exceptions=True # Continue despite failures
)
Debugging Async Code
Enable Debug Mode
# Add this to see detailed async debugging info
import asyncio
asyncio.run(main(), debug=True)
Check for Unawaited Coroutines
Python will warn you about coroutines that weren’t awaited:
RuntimeWarning: coroutine 'fetch_data' was never awaited
Use asyncio.current_task()
for debugging
async def debug_example():
current = asyncio.current_task()
print(f"Current task: {current.get_name()}")
all_tasks = asyncio.all_tasks()
print(f"Total running tasks: {len(all_tasks)}")
When Async Makes Sense (And When It Doesn’t)
After writing your first async programs, you might wonder when to use these patterns: Async shines when you’re waiting for things:
- Network requests (APIs, web scraping)
- Database queries
- File operations (reading/writing large files)
- User input or external events
Skip async for:
- CPU-intensive tasks (mathematical calculations, image processing)
- Simple scripts that do one thing at a time
- Legacy code where integration complexity outweighs benefits
The async functions you’ll use most:
asyncio.run()
- Start the event loopasyncio.create_task()
- Schedule coroutine for executionasyncio.gather()
- Wait for multiple operationsasyncio.wait_for()
- Add timeout to operationsawait
- Pause until operation completes
Next Steps
In Part 4, we’ll dive deeper into the event loop - the engine that makes all this concurrency possible. You’ll learn how it schedules tasks, handles I/O, and coordinates your async operations.