Getting Started with Python Async Programming
Build faster Python applications by mastering async programming and learning how to handle I/O bound workloads efficiently with real world examples.
Image by Author
# Introduction
Most Python applications spend significant time waiting on APIs, databases, file systems, and network services. Async programming allows a program to pause while waiting for I/O operations and continue executing other tasks instead of blocking.
In this tutorial, you will learn the fundamentals of async programming in Python using clear code examples. We will compare synchronous and asynchronous execution, explain how the event loop works, and apply async patterns to real-world scenarios such as concurrent API requests and background tasks.
By the end of this guide, you will understand when async programming is useful, how to use async and await correctly, and how to write scalable and reliable async Python code.
# Defining Async Programming in Python
Async programming allows a program to pause execution while waiting for an operation to complete and continue executing other tasks in the meantime.
Core building blocks include:
- async def for defining coroutines
- await for non-blocking waits
- The event loop for task scheduling
Note: Async programming improves throughput, not raw computation speed.
# Understanding the Async Event Loop in Python
The event loop is responsible for managing and executing asynchronous tasks.
Key responsibilities include:
- Tracking paused and ready tasks
- Switching execution when tasks await I/O
- Coordinating concurrency without threads
Python uses the asyncio library as its standard async runtime.
# Comparing Sequential vs. Async Execution in Python
This section demonstrates how blocking sequential code compares to asynchronous concurrent execution and how async reduces total waiting time for I/O-bound tasks.
// Examining a Sequential Blocking Example
Sequential execution runs tasks one after another. If a task performs a blocking operation, the entire program waits until that operation completes. This approach is simple but inefficient for I/O-bound workloads where waiting dominates execution time.
This function simulates a blocking task. The call to time.sleep pauses the entire program for the specified number of seconds.
The timer starts before the function calls and stops after all three calls complete. Each function runs only after the previous one finishes.
Output:
- file-1 starts and blocks the program for two seconds
- file-2 starts only after file-1 finishes
- file-3 starts only after file-2 finishes
Total runtime is the sum of all delays, approximately six seconds.
// Examining an Asynchronous Concurrent Example
Asynchronous execution allows tasks to run concurrently. When a task reaches an awaited I/O operation, it pauses and allows other tasks to continue. This overlapping of waiting time significantly improves throughput.
This async function defines a coroutine. The await asyncio.sleep call pauses only the current task, not the entire program.
asyncio.gather schedules all three coroutines to run concurrently on the event loop.
This starts the event loop and executes the async program.
Output:
- All three tasks start almost at the same time
- Each task waits independently for two seconds
- While one task is waiting, others continue executing
- Total runtime is close to the longest single delay, approximately two seconds
# Exploring How Await Works in Python Async Code
The await keyword tells Python that a coroutine may pause and allow other tasks to run.
Incorrect usage:
Correct usage:
Failing to use await prevents concurrency and may cause runtime warnings.
# Running Multiple Async Tasks Using asyncio.gather
asyncio.gather allows multiple coroutines to run concurrently and collects their results once all tasks have completed. It is commonly used when several independent async operations can be executed in parallel.
The job coroutine simulates an asynchronous task. It prints a start message, waits for one second using a non-blocking sleep, then prints a finish message and returns a result.
asyncio.gather schedules all three jobs to run concurrently on the event loop. Each job begins execution immediately until it reaches an awaited operation.
Output:
- All three jobs start almost at the same time
- Each job waits independently for one second
- While one job is waiting, others continue running
- The results are returned in the same order the tasks were passed to asyncio.gather
- Total execution time is close to one second, not three
This pattern is foundational for concurrent network requests, database queries, and other I/O-bound operations.
# Making Concurrent HTTP Requests
Async HTTP requests are a common real-world use case where async programming provides immediate benefits. When multiple APIs are called sequentially, total execution time becomes the sum of all response delays. Async allows these requests to run concurrently.
This list contains three URLs that intentionally delay their responses by one, two, and three seconds.
This function performs a blocking HTTP request using the standard library. It cannot be awaited directly.
The fetch coroutine measures execution time and logs when a request starts. The blocking HTTP request is offloaded to a background thread using asyncio.to_thread. This prevents the event loop from blocking.
All requests are scheduled concurrently using asyncio.gather.
Output:
- All three HTTP requests start almost immediately
- Each request completes after its own delay
- The longest request determines the total wall time
- Total runtime is approximately three and a half seconds, not the sum of all delays
This approach significantly improves performance when calling multiple APIs and is a common pattern in modern async Python services.
# Implementing Error Handling Patterns in Async Python Applications
Robust async applications must handle failures gracefully. In concurrent systems, a single failing task should not cause the entire workflow to fail. Proper error handling ensures that successful tasks complete while failures are reported cleanly.
This list includes two successful endpoints and one endpoint that returns an HTTP 404 error.
This function performs a blocking HTTP request with a timeout. It may raise exceptions such as timeouts or HTTP errors.
This function wraps a blocking HTTP request in a safe asynchronous interface. The blocking operation is executed in a background thread using asyncio.to_thread, which prevents the event loop from stalling while the request is in progress.
Common failure cases such as timeouts and HTTP errors are caught and converted into structured responses. This ensures that errors are handled predictably and that a single failing request does not interrupt the execution of other concurrent tasks.
All requests are executed concurrently using asyncio.gather.
Output:
- The first two requests complete successfully and return parsed JSON data
- The third request returns a structured error instead of raising an exception
- All results are returned together without interrupting the workflow
This pattern ensures that a single failing request does not break the entire async operation and is essential for production-ready async applications.
# Using Async Programming in Jupyter Notebooks
Jupyter notebooks already run an active event loop. Because of this, asyncio.run cannot be used inside a notebook cell, as it attempts to start a new event loop while one is already running.
This async function simulates a simple non-blocking task using asyncio.sleep.
Incorrect usage in notebooks:
Correct usage in notebooks:
Understanding this distinction ensures async code runs correctly in Jupyter notebooks and prevents common runtime errors when experimenting with asynchronous Python.
# Controlling Concurrency with Async Semaphores
External APIs and services often enforce rate limits, which makes it unsafe to run too many requests at the same time. Async semaphores allow you to control how many tasks execute concurrently while still benefiting from asynchronous execution.
The semaphore is initialized with a limit of two, meaning only two tasks can enter the protected section at the same time.
The task function represents an asynchronous unit of work. Each task must acquire the semaphore before executing, and if the limit has been reached, it waits until a slot becomes available.
Once inside the semaphore, the task records its start time, prints a start message, and awaits a two-second non-blocking sleep to simulate an I/O-bound operation. After the sleep completes, the task calculates its execution time, prints a completion message, and releases the semaphore.
The main function schedules four tasks to run concurrently using asyncio.gather, but the semaphore ensures that they execute in two waves of two tasks.
Finally, asyncio.run starts the event loop and runs the program, resulting in a total execution time of approximately four seconds.
Output:
- Tasks 1 and 2 start first due to the semaphore limit
- Tasks 3 and 4 wait until a slot becomes available
- Tasks execute in two waves, each lasting two seconds
- Total wall time is approximately four seconds
Semaphores provide an effective way to enforce concurrency limits and protect system stability in production async applications.
# Concluding Remarks
Async programming is not a universal solution. It is not suitable for CPU-intensive workloads such as machine learning training, image processing, or numerical simulations. Its strength lies in handling I/O-bound operations where waiting time dominates execution.
When used correctly, async programming improves throughput by allowing tasks to make progress while others are waiting. Proper use of await is essential for concurrency, and async patterns are especially effective in API-driven and service-based systems.
In production environments, controlling concurrency and handling failures explicitly are critical to building reliable and scalable async Python applications.
Abid Ali Awan (@1abidaliawan) is a certified data scientist professional who loves building machine learning models. Currently, he is focusing on content creation and writing technical blogs on machine learning and data science technologies. Abid holds a Master's degree in technology management and a bachelor's degree in telecommunication engineering. His vision is to build an AI product using a graph neural network for students struggling with mental illness.
Get the FREE ebook 'KDnuggets Artificial Intelligence Pocket Dictionary' along with the leading newsletter on Data Science, Machine Learning, AI & Analytics straight to your inbox.
By subscribing you accept KDnuggets Privacy Policy