Python await Keyword: How It Works
python await keyword: Learn how the Python await keyword suspends coroutines, what can be awaited, and how to avoid common pitfalls in asyncio code.
python await keyword requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The await keyword in Python suspends the execution of a coroutine until the awaited awaitable completes, allowing the event loop to run other tasks in the meantime. It is the primary mechanism for writing concurrent code with asyncio, and understanding how it behaves is essential for building responsive applications.
What await Does Inside a Coroutine
await is valid only inside an async def function. When you write await some_awaitable, the coroutine pauses at that point and control returns to the event loop. The event loop can then run other pending tasks. When the awaited object completes, the coroutine resumes and the expression evaluates to the result of that awaitable.
Consider a minimal example:
import asyncio async def fetch_data(): await asyncio.sleep(1) # Simulate I/O return "data" async def main(): result = await fetch_data() print(result) asyncio.run(main())
Here, asyncio.sleep(1) is an awaitable that yields control for one second. While the coroutine is suspended, the event loop can execute other tasks if any exist. Once the sleep finishes, fetch_data resumes and returns the string, which is then assigned to result in main.
What Can Be Awaited
Three types of objects are directly awaitable: coroutines, asyncio.Task instances, and asyncio.Futures. Any object that implements the __await__ method can also be awaited, but in practice you will mostly encounter the first three.
A coroutine object is created when you call an async def function. It must be awaited or scheduled as a task; otherwise, Python issues a runtime warning. A Task wraps a coroutine and schedules it on the event loop. A Future represents a result that will be available later.
The following example shows awaiting each type:
import asyncio async def coro(): return 42 async def main(): # Await a coroutine directly result = await coro() print(result) # Await a Task task = asyncio.create_task(coro()) result = await task print(result) # Await a Future future = asyncio.Future() future.set_result(100) value = await future print(value) asyncio.run(main())
Note that a coroutine object can be awaited only once. If you try to await the same coroutine object twice, you will get a runtime error because the coroutine is already consumed.
How await Interacts with the Event Loop
await is a suspension point, not a blocking call. The event loop runs in a single thread and uses cooperative multitasking. When a coroutine hits await, it voluntarily gives up control. The event loop then decides which task to run next based on its scheduling logic. This design allows many concurrent tasks to share a single thread without the overhead of thread switching.
Because the event loop is single-threaded, you must never perform blocking operations inside a coroutine. A blocking call such as time.sleep(1) will halt the entire event loop, preventing other tasks from running. Instead, use asyncio.sleep or other non-blocking alternatives that yield control.
Common Mistakes and How to Avoid Them
One frequent error is calling await on a regular function. Since a regular function is not awaitable, this raises a TypeError. For example:
# Wrong: regular function is not awaitable def get_value(): return 1 async def main(): value = await get_value() # TypeError
Another mistake is forgetting to await a coroutine. If you call an async def function without await, you get a coroutine object that is never executed. Python may emit a RuntimeWarning if the coroutine is garbage-collected without being awaited.
async def main(): coro() # Coroutine created but not awaited
Using await outside an async def function is a SyntaxError. The await keyword is only meaningful inside a coroutine.
Finally, mixing blocking code with await defeats the purpose of async programming. For instance, using time.sleep inside a coroutine blocks the event loop. Replace it with asyncio.sleep or offload the blocking call to a thread using asyncio.to_thread.
Await and Blocking Code: Performance Implications
The main performance concern with await is not the keyword itself but what you await. If you await a coroutine that internally performs blocking I/O, the entire event loop stalls. This is a common source of poor performance in asyncio applications.
For example, a synchronous HTTP request using requests will block the thread. To keep the event loop responsive, you should use an asynchronous HTTP client like aiohttp, or run the blocking call in a separate thread with asyncio.to_thread:
import asyncio import requests async def fetch_url(url): response = await asyncio.to_thread((requests.get, url) return response.text
n
This offloads the blocking requests call to a thread pool, allowing the event loop to continue running other tasks while the request is in flight.
Timeouts and Cancellation with await
When you await a long-running task, you may want to impose a timeout. asyncio.wait_for wraps an awaitable and raises asyncio.TimeoutError if it does not complete within the given duration.
import asyncio async def slow_operation(): await asyncio.sleep(10) return "done" async def main(): try: result = await asyncio.wait_for(slow_operation(), timeout=2) except asyncio.TimeoutError: print("Operation timed out")
When a timeout occurs, the awaited task is cancelled. If you need to protect a task from cancellation, use asyncio.shield. This is useful when you want to allow a timeout on the outer operation but keep the inner task running in the background.
Additionally, awaiting a Task can raise asyncio.CancelledError if the task is cancelled externally. You should handle this exception if your coroutine needs to perform cleanup before exiting.
Practical Example: Combining Multiple Awaits
In real applications, you often need to run several independent coroutines concurrently. asyncio.gather schedules all provided awaitables and returns a list of results when all complete.
import asyncio async def fetch(url): await asyncio.sleep(1) return f"Result from {url}" async def main(): urls = ["a.com", "b.com", "c.com"] results = await asyncio.gather(*(fetch(url) for url in urls)) print(results) asyncio.run(main())
Here, all three fetch coroutines are scheduled at the same time. Each one awaits asyncio.sleep(1), and the event loop interleaves them. The total execution time is roughly one second, not three, because the sleeps happen concurrently.
asyncio.gather also propagates exceptions. If any of the awaited coroutines raises an exception, the exception is raised in the await expression. You can use return_exceptions=True to collect exceptions as results instead.
Understanding how await works at this level lets you write concurrent code that is both efficient and predictable. The key is to remember that await is a suspension point, not a blocking call, and to choose the appropriate awaitable for each scenario.