async and await¶
The async Keyword¶
The async keyword defines a coroutine function. Calling it returns a coroutine object, not the result.
async def greet(name):
return f"Hello, {name}"
# Calling returns a coroutine object
coro = greet("Alice")
print(coro) # <coroutine object greet at 0x...>
# Must run it to get the result
import asyncio
result = asyncio.run(greet("Alice"))
print(result) # "Hello, Alice"
The await Keyword¶
await suspends the coroutine until the awaited operation completes. It can only be used inside async functions.
async def fetch_data():
print("Starting fetch...")
await asyncio.sleep(1) # Suspend here, let other tasks run
print("Fetch complete!")
return {"data": 42}
async def main():
result = await fetch_data() # Wait for fetch_data to complete
print(result)
asyncio.run(main())
Output:
Starting fetch...
Fetch complete!
{'data': 42}
What Can Be Awaited?¶
Only awaitable objects can be used with await:
1. Coroutines¶
async def my_coroutine():
return "result"
async def main():
result = await my_coroutine() # ✅ Coroutine is awaitable
2. Tasks¶
async def main():
task = asyncio.create_task(my_coroutine())
result = await task # ✅ Task is awaitable
3. Futures¶
async def main():
future = asyncio.Future()
# ... something sets future.set_result(value)
result = await future # ✅ Future is awaitable
4. Objects with __await__¶
class MyAwaitable:
def __await__(self):
yield
return "custom result"
async def main():
result = await MyAwaitable() # ✅ Has __await__
Sequential vs Concurrent Execution¶
Sequential (Slow)¶
async def fetch(url):
await asyncio.sleep(1) # Simulate network delay
return f"Data from {url}"
async def main():
# Each await blocks until complete
result1 = await fetch("url1") # 1 second
result2 = await fetch("url2") # 1 second
result3 = await fetch("url3") # 1 second
# Total: 3 seconds
asyncio.run(main())
Concurrent (Fast)¶
async def main():
# Create tasks - starts all immediately
task1 = asyncio.create_task(fetch("url1"))
task2 = asyncio.create_task(fetch("url2"))
task3 = asyncio.create_task(fetch("url3"))
# Await results
result1 = await task1
result2 = await task2
result3 = await task3
# Total: ~1 second (parallel execution)
asyncio.run(main())
Using gather (Preferred)¶
async def main():
results = await asyncio.gather(
fetch("url1"),
fetch("url2"),
fetch("url3")
)
# results = ["Data from url1", "Data from url2", "Data from url3"]
# Total: ~1 second
asyncio.run(main())
Common Mistakes¶
1. Forgetting await¶
async def main():
result = fetch_data() # ❌ Missing await - returns coroutine object
print(result) # <coroutine object fetch_data at 0x...>
async def main():
result = await fetch_data() # ✅ Correct
print(result) # Actual result
Warning: Python shows RuntimeWarning: coroutine was never awaited
2. Using await Outside async Function¶
def main():
result = await fetch_data() # ❌ SyntaxError
async def main():
result = await fetch_data() # ✅ Must be inside async function
3. Blocking the Event Loop¶
import time
async def bad_example():
time.sleep(1) # ❌ Blocks the entire event loop!
return "done"
async def good_example():
await asyncio.sleep(1) # ✅ Yields control to event loop
return "done"
4. Not Running Coroutines¶
async def my_task():
print("Running")
# ❌ Does nothing - coroutine created but never run
my_task()
# ✅ Actually runs the coroutine
asyncio.run(my_task())
Async Context Managers¶
Use async with for async setup/cleanup:
class AsyncConnection:
async def __aenter__(self):
print("Connecting...")
await asyncio.sleep(0.1)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Disconnecting...")
await asyncio.sleep(0.1)
async def main():
async with AsyncConnection() as conn:
print("Using connection")
asyncio.run(main())
Output:
Connecting...
Using connection
Disconnecting...
Real-World Example¶
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session: # Async context manager
async with session.get(url) as response: # Another one
return await response.text()
Async Iterators¶
Use async for to iterate over async iterables:
class AsyncCounter:
def __init__(self, stop):
self.stop = stop
def __aiter__(self):
self.current = 0
return self
async def __anext__(self):
if self.current >= self.stop:
raise StopAsyncIteration
await asyncio.sleep(0.1)
self.current += 1
return self.current
async def main():
async for num in AsyncCounter(5):
print(num)
asyncio.run(main())
Async Generator (Simpler)¶
async def async_counter(stop):
for i in range(stop):
await asyncio.sleep(0.1)
yield i
async def main():
async for num in async_counter(5):
print(num)
Async Comprehensions¶
# Async list comprehension
async def main():
results = [await fetch(url) async for url in async_url_generator()]
# Async generator expression
async def main():
gen = (await fetch(url) async for url in async_url_generator())
Summary¶
| Syntax | Purpose |
|---|---|
async def |
Define a coroutine function |
await |
Suspend until awaitable completes |
async with |
Async context manager |
async for |
Iterate over async iterable |
asyncio.run() |
Entry point to run async code |
Key Rules:
awaitcan only be used insideasyncfunctions- Only awaitables can be awaited (coroutines, tasks, futures)
- Don't use blocking calls (
time.sleep,requests.get) in async code - Use
asyncio.gather()for concurrent execution - Always
awaityour coroutines or they won't run
Runnable Example: async_await_tutorial.py¶
"""
Beginner Tutorial 1: Introduction to async/await Syntax
This file introduces the fundamental async/await syntax in Python.
We'll learn:
- What makes a function "async"
- How to call async functions with "await"
- The difference between sync and async execution
- Basic asyncio.run() usage
Learning objectives:
1. Understand async def syntax
2. Learn when and how to use await
3. See the difference between blocking and non-blocking code
"""
import asyncio
import time
# ============================================================================
# PART 1: Understanding Synchronous vs Asynchronous
# ============================================================================
def synchronous_function():
"""
A regular (synchronous) function.
When called, it runs from start to finish without yielding control.
"""
print("Starting synchronous function")
time.sleep(1) # This BLOCKS the entire program for 1 second
print("Finished synchronous function")
return "Sync result"
async def asynchronous_function():
"""
An asynchronous function (coroutine).
Defined with 'async def' instead of just 'def'.
When called WITHOUT await, it returns a coroutine object.
When called WITH await, it executes and returns the result.
"""
print("Starting asynchronous function")
# asyncio.sleep() is non-blocking - it yields control to event loop
await asyncio.sleep(1)
print("Finished asynchronous function")
return "Async result"
# ============================================================================
# PART 2: Basic async/await Patterns
# ============================================================================
async def simple_async_example():
"""
A simple async function demonstrating await.
Key points:
- async def creates a coroutine function
- await pauses this function and lets other tasks run
- await can only be used inside async functions
"""
print("Step 1: Before await")
# await pauses execution here and yields to event loop
# Other tasks could run during this 1 second
await asyncio.sleep(1)
print("Step 2: After await")
return "Complete!"
async def calling_async_functions():
"""
Demonstrates how to call one async function from another.
Important:
- To get the result, you must use 'await'
- Without 'await', you just get a coroutine object
"""
print("\n--- Calling async functions ---")
# WRONG: Without await, this just creates a coroutine object
# It doesn't actually run the function!
coro = asynchronous_function()
print(f"Without await: {coro}")
print(f"Type: {type(coro)}")
# We need to clean up the coroutine we created
coro.close()
# CORRECT: With await, the function runs and we get the result
result = await asynchronous_function()
print(f"With await: {result}")
return result
# ============================================================================
# PART 3: Sequential Async Execution
# ============================================================================
async def fetch_data(data_id: int, delay: float):
"""
Simulates fetching data from a source (like a database or API).
Args:
data_id: Identifier for the data
delay: How long the fetch takes (simulated)
Returns:
The fetched data
"""
print(f" Fetching data {data_id}...")
await asyncio.sleep(delay) # Simulate I/O operation
print(f" Data {data_id} retrieved!")
return f"Data_{data_id}"
async def sequential_execution():
"""
Demonstrates sequential async execution.
Even though we're using async/await, if we await each operation
one after another, they still run sequentially (one at a time).
This is like having async superpowers but choosing not to use them!
"""
print("\n--- Sequential Execution ---")
start_time = time.time()
# Each await blocks until the operation completes
# Total time: 1 + 1 + 1 = 3 seconds
data1 = await fetch_data(1, 1.0)
data2 = await fetch_data(2, 1.0)
data3 = await fetch_data(3, 1.0)
elapsed = time.time() - start_time
print(f"Sequential execution took {elapsed:.2f} seconds")
print(f"Retrieved: {data1}, {data2}, {data3}")
return [data1, data2, data3]
# ============================================================================
# PART 4: Understanding await Behavior
# ============================================================================
async def demonstrate_await_behavior():
"""
Shows what happens during await - control is yielded to event loop.
When you 'await' something:
1. Current function pauses
2. Control returns to event loop
3. Event loop can run other tasks
4. When awaited operation completes, function resumes
"""
print("\n--- Await Behavior ---")
print("Before first await")
await asyncio.sleep(0.5) # Pause for 0.5 seconds
print("Between awaits")
await asyncio.sleep(0.5) # Pause for another 0.5 seconds
print("After second await")
# Multiple awaits in sequence
results = []
for i in range(3):
print(f" Iteration {i+1}")
await asyncio.sleep(0.1)
results.append(i)
return results
# ============================================================================
# PART 5: Async Function Return Values
# ============================================================================
async def async_function_with_return():
"""
Async functions can return values just like regular functions.
The return value is accessed by awaiting the function call.
"""
await asyncio.sleep(0.1)
return {"status": "success", "value": 42}
async def async_function_with_error():
"""
Async functions can raise exceptions.
These exceptions are raised when the function is awaited.
"""
await asyncio.sleep(0.1)
raise ValueError("Something went wrong!")
async def handling_returns_and_errors():
"""
Demonstrates returning values and handling errors in async functions.
"""
print("\n--- Returns and Errors ---")
# Getting return value
result = await async_function_with_return()
print(f"Received result: {result}")
# Handling errors
try:
await async_function_with_error()
except ValueError as e:
print(f"Caught exception: {e}")
# ============================================================================
# PART 6: Common Mistakes and Gotchas
# ============================================================================
async def common_mistakes():
"""
Demonstrates common mistakes when learning async/await.
"""
print("\n--- Common Mistakes ---")
# Mistake 1: Forgetting await
print("Mistake 1: Forgetting await")
coro = asyncio.sleep(1) # This creates a coroutine but doesn't run it
print(f" Without await: {coro}")
coro.close() # Clean up
# Correct way
print(" Correct way with await:")
await asyncio.sleep(0.1)
print(" Sleep completed!")
# Mistake 2: Using time.sleep instead of asyncio.sleep
print("\nMistake 2: Using time.sleep (blocking)")
print(" DON'T do this in async code:")
print(" time.sleep(1) # This blocks the entire event loop!")
print(" DO this instead:")
await asyncio.sleep(0.1) # Non-blocking
print(" asyncio.sleep completed!")
# Mistake 3: Trying to await non-async functions
print("\nMistake 3: Can't await regular functions")
print(" Regular functions must be called normally")
regular_result = synchronous_function() # No await!
print(f" Result: {regular_result}")
# ============================================================================
# PART 7: Running Async Code
# ============================================================================
async def main():
"""
Main async function that demonstrates all concepts.
This is the entry point for our async program.
asyncio.run() creates an event loop, runs this coroutine,
and then closes the event loop.
"""
print("=" * 70)
print("ASYNC/AWAIT BASICS TUTORIAL")
print("=" * 70)
# Run basic example
result = await simple_async_example()
print(f"Result: {result}")
# Call async functions
await calling_async_functions()
# Sequential execution
await sequential_execution()
# Await behavior
await demonstrate_await_behavior()
# Returns and errors
await handling_returns_and_errors()
# Common mistakes
await common_mistakes()
print("\n" + "=" * 70)
print("Tutorial complete!")
print("=" * 70)
# ============================================================================
# Running the code
# ============================================================================
if __name__ == "__main__":
"""
asyncio.run() is the recommended way to run async programs.
What it does:
1. Creates a new event loop
2. Runs the provided coroutine (main)
3. Closes the event loop when done
This should be called only once per program, at the top level.
Don't call asyncio.run() inside async functions!
"""
# This is the standard way to run async code
asyncio.run(main())
# Note: You can't do this in an async function:
# async def some_function():
# asyncio.run(main()) # ❌ Error! Already in async context
"""
KEY TAKEAWAYS:
1. async def: Defines an asynchronous function (coroutine)
2. await: Pauses execution and yields control to event loop
3. asyncio.run(): Entry point to run async code (use once)
4. await can only be used inside async functions
5. Async functions return coroutines that must be awaited
6. Use asyncio.sleep(), not time.sleep() in async code
MENTAL MODEL:
- Think of async/await as a way to write concurrent code that looks synchronous
- 'await' is like saying "I'm going to wait here, you can do other stuff"
- The event loop is the conductor that switches between tasks
NEXT STEPS:
- Learn about coroutines in detail (02_first_coroutine.py)
- Understand the event loop (03_event_loop_basics.py)
- Run multiple tasks concurrently (04_multiple_coroutines.py)
"""