`async` and `await`: Simplifying Asynchronous Code
We've learned about the core components of asyncio: the event loop that manages everything, coroutines that define the work, and tasks that schedule the work to run concurrently. Now, let's focus on the two keywords that make writing this code so elegant and readable: async and await.
These keywords, introduced in Python 3.5, are "syntactic sugar." They don't add new functionality that wasn't already possible, but they provide a much cleaner and more intuitive syntax for writing and reading asynchronous code, making it look almost like standard synchronous code.
📚 Prerequisites
You should understand the basic concepts of coroutines, tasks, and the event loop.
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅
async def: How this syntax transforms a regular function into a native coroutine. - ✅
await: Understand its crucial role in pausing a coroutine and yielding control to the event loop. - ✅ Awaitable Objects: Learn what kinds of objects can be used with
await. - ✅ Reading Async Code: See how
async/awaitmakes the flow of asynchronous logic much easier to follow.
🧠 Section 1: async def - Defining a Coroutine
The async def syntax is simple: it marks a function as a coroutine.
# This is a regular function
def regular_function():
return "I am a regular function."
# This is a coroutine
async def my_coroutine():
return "I am a coroutine."
The key difference is what happens when you call them:
- Calling
regular_function()executes the code immediately and returns the string. - Calling
my_coroutine()does not execute the code. It immediately returns a coroutine object.
This coroutine object is a blueprint for the work that needs to be done, ready to be handed off to the event loop.
💻 Section 2: await - The Pause Button
The await keyword can only be used inside an async def function. Its job is to pause the execution of the current coroutine and wait for something to complete. That "something" is called an awaitable object.
While the coroutine is paused at an await point, the event loop is free to run other tasks.
What is an Awaitable? There are three main types of awaitable objects:
- A Coroutine: You can
awaitanother coroutine. This is how you chain asynchronous operations together. - A Task: As we saw in the last article, a Task (
asyncio.create_task()) is an awaitable that runs a coroutine concurrently. - A Future: A lower-level object that represents the eventual result of an asynchronous operation. Tasks are a type of Future.
Example: Chaining Coroutines Let's build a simple example where one coroutine calls another.
import asyncio
import time
async def fetch_data():
"""A coroutine that simulates a network request."""
print("Start fetching data...")
# This is our I/O-bound operation.
# While this is 'sleeping', the event loop could run other tasks.
await asyncio.sleep(2)
print("...done fetching data.")
return {"data": "some_value"}
async def process_data():
"""A coroutine that waits for data and then processes it."""
print("About to call fetch_data...")
# The process_data coroutine pauses here until fetch_data completes.
result = await fetch_data()
print(f"Data received: {result}")
asyncio.run(process_data())
Output:
About to call fetch_data...
Start fetching data...
...done fetching data.
Data received: {'data': 'some_value'}
The await fetch_data() expression pauses process_data and waits for the fetch_data coroutine to finish its work and return a value.
🛠️ Section 3: Making Code Look Synchronous
The real beauty of async/await is how it makes complex, non-blocking code read like a simple, synchronous script.
Consider our example from the last section. The logic inside process_data is perfectly clear:
- Print a message.
- Get a result from
fetch_data. - Print the result.
# This...
result = await fetch_data()
# ...looks almost identical to this synchronous code:
# result = get_data_from_somewhere()
Without async/await, this kind of asynchronous logic would require complex callbacks or other patterns that are much harder to read and debug. The async/await syntax allows us to write concurrent code that follows a clear, linear path, which is a massive improvement in readability and maintainability.
Let's look at our concurrent download example again, this time focusing on the clarity of the main function.
async def download_page_async(url):
# ... (implementation from previous article) ...
await asyncio.sleep(1)
print(f"Finished: {url}")
async def main():
# Create tasks to run them concurrently
task1 = asyncio.create_task(download_page_async("page1"))
task2 = asyncio.create_task(download_page_async("page2"))
# The code looks sequential, but the work is happening concurrently.
# We are just waiting for the results here.
await task1
await task2
asyncio.run(main())
The main function clearly expresses its intent: schedule two tasks, then wait for both of them to be completed.
✨ Conclusion & Key Takeaways
The async and await keywords are the user-friendly interface to Python's powerful asyncio framework. They allow you to write high-performance, concurrent I/O-bound code without the complexities of traditional asynchronous patterns.
Let's summarize the key takeaways:
async defdefines a function as a coroutine, which can be paused.awaitis the keyword that pauses the coroutine, yields control to the event loop, and waits for an awaitable object (like another coroutine or a task) to complete.- Readability: The primary benefit of
async/awaitis that it makes asynchronous code look and feel like synchronous code, making it much easier to reason about.
➡️ Next Steps
Now that you have a firm grasp of the syntax and the core components, the next step is to learn how to manage multiple concurrent tasks more effectively. In the next article, we'll explore "Running Concurrent Tasks: asyncio.gather()," a powerful tool for running and collecting results from many coroutines at once.
Happy awaiting!