Back to Python
2026-01-058 min read

Async Mistakes (Python Programming)

Learn Async Mistakes (Python Programming) step by step with clear examples and exercises.

Title: Async Mistakes (Python Programming)

Why This Matters

In this lesson, we'll delve into common mistakes when working with asynchronous programming in Python. Understanding these pitfalls will help you write cleaner, more efficient code and avoid frustrating bugs that can derail your projects. Whether you're preparing for a technical interview or tackling real-world programming challenges, mastering async programming is essential to becoming a well-rounded Python developer.

Prerequisites

To follow along with this lesson, you should have a basic understanding of the following topics:

  • Python syntax and data types
  • Synchronous programming in Python
  • Basic concepts of asynchronous programming (e.g., event loops, coroutines)

Core Concept

Python's async/await syntax simplifies writing asynchronous code by allowing you to structure your functions as coroutines, which can be scheduled and executed by an event loop. This approach enables non-blocking I/O operations, making it possible to perform multiple tasks concurrently without tying up system resources.

However, working with async code can introduce new challenges that may not be immediately apparent when writing synchronous code. In this section, we'll explore some common mistakes and best practices for avoiding them.

Misunderstanding the role of await

The await keyword is used to pause the execution of a coroutine until a particular task completes. It signals to Python that the function should be yielded back to the event loop, allowing other tasks to run in the meantime.

async def fetch_data(url):
response = await requests.get(url)
data = await response.json()
return data

In the example above, await is used twice: once for waiting on the HTTP request to complete and another time for parsing the JSON response. If you forget to use await when calling an asynchronous function, your code will still run synchronously, defeating the purpose of using async/await in the first place.

async def fetch_data(url):
response = requests.get(url) # No await here!
data = await response.json()
return data

Ignoring exceptions

When working with asynchronous code, it's essential to handle exceptions properly to ensure your program doesn't crash unexpectedly. Since async functions can be yielded back to the event loop at any time, you should use try and except blocks to catch and handle potential errors gracefully.

async def fetch_data(url):
try:
response = await requests.get(url)
data = await response.json()
return data
except requests.exceptions.RequestException as e:
print(f"Error fetching data: {e}")
raise

In the example above, we catch any requests.exceptions.RequestException that might occur during the HTTP request or JSON parsing and print an error message before re-raising the exception to propagate it up the call stack.

Mixing synchronous and asynchronous code

When working with async functions, it's crucial to ensure that your entire function is written using async/await syntax. If you mix synchronous and asynchronous code within a single function, you may encounter unexpected behavior or performance issues due to the event loop's scheduling mechanism.

async def fetch_data(url):
response = requests.get(url) # Synchronous call!
data = await response.json()
return data

In the example above, we've mixed synchronous and asynchronous code by calling requests.get() synchronously instead of using await. This will prevent the event loop from scheduling other tasks while waiting for the HTTP request to complete, reducing the overall concurrency of your program.

Using global variables improperly

When working with async functions, it's essential to be mindful of how you use and manage global variables. Since multiple coroutines may access and modify shared state simultaneously, using globals can lead to race conditions, inconsistent behavior, or other hard-to-debug issues.

counter = 0

async def increment_counter():
global counter
counter += 1

async def print_counter():
for _ in range(5):
await asyncio.sleep(1)
print(counter)
await increment_counter()

In the example above, we've used a global variable counter to keep track of the number of times the counter is incremented and printed. However, this approach can lead to race conditions if multiple coroutines access and modify the shared state simultaneously. A better approach would be to use a dedicated async context manager or a concurrent data structure designed for safe multi-producer, multi-consumer scenarios.

Misusing asyncio.gather()

The asyncio.gather() function allows you to run multiple coroutines concurrently and wait for all of them to complete. However, using it indiscriminately can lead to unnecessary complexity or performance issues if not used correctly.

async def fetch_data(url):
response = await requests.get(url)
data = await response.json()
return data

async def main():
urls = ["https://example.com", "https://example2.com"]
results = await asyncio.gather(*[fetch_data(url) for url in urls])
print(results)

In the example above, we've used asyncio.gather() to fetch data from multiple URLs concurrently. This is a good use case for the function, as it simplifies the code and allows us to perform I/O operations more efficiently. However, if you have a large number of tasks or complex dependencies between them, using asyncio.gather() may not be the best solution, as it can lead to increased complexity and potential deadlocks.

Worked Example

In this section, we'll walk through a worked example that demonstrates common async mistakes and how to avoid them. We'll fetch data from multiple URLs concurrently using asyncio.gather() while handling exceptions and avoiding race conditions.

import asyncio
import requests

async def fetch_data(url):
try:
response = await requests.get(url)
data = await response.json()
return data
except requests.exceptions.RequestException as e:
print(f"Error fetching data from {url}: {e}")
return None

async def main():
urls = ["https://example.com", "https://example2.com"]
results = await asyncio.gather(*[fetch_data(url) for url in urls])
print("Results:", results)

if __name__ == "__main__":
try:
await main()
except KeyboardInterrupt:
print("\nInterrupted! Shutting down...")

In the example above, we've defined an async def fetch_data(url) function that fetches data from a given URL and handles exceptions properly. We then use asyncio.gather() to run multiple instances of this function concurrently in the main() function. By handling exceptions and using await correctly, we've ensured that our code is both efficient and robust.

Common Mistakes

  1. Forgetting to use await when calling asynchronous functions
async def fetch_data(url):
response = requests.get(url) # No await here!
data = await response.json()
return data
  1. Ignoring exceptions
async def fetch_data(url):
response = requests.get(url)
data = await response.json()
return data
  1. Mixing synchronous and asynchronous code within a single function
async def fetch_data(url):
response = requests.get(url) # Synchronous call!
data = await response.json()
return data
  1. Using global variables improperly
counter = 0

async def increment_counter():
global counter
counter += 1

async def print_counter():
for _ in range(5):
await asyncio.sleep(1)
print(counter)
await increment_counter()
  1. Misusing asyncio.gather()
async def fetch_data(url):
response = requests.get(url) # Synchronous call!
data = await response.json()
return data

async def main():
urls = ["https://example.com", "https://example2.com"]
results = await asyncio.gather(*[fetch_data(url) for url in urls])
print(results)

Practice Questions

  1. Rewrite the following synchronous function to use async/await syntax:
def fetch_data(url):
response = requests.get(url)
data = response.json()
return data
  1. Modify the fetch_data() function from question 1 to handle exceptions properly using a try and except block.
  2. Write an asynchronous function that fetches data from multiple URLs concurrently using asyncio.gather(). The function should return a list of dictionaries, where each dictionary contains the URL and the corresponding data.
  3. Implement a simple async context manager that ensures safe access to shared resources (e.g., a database connection) by using locks or semaphores.
  4. Write an asynchronous generator function that produces Fibonacci numbers up to a given limit. Use asyncio.as_completed() to consume the results in the correct order.

FAQ

  1. Why should I use async/await instead of threads for concurrent programming?

Async/await is generally preferred over threads for concurrent programming in Python because it simplifies the code, reduces context switching overhead, and provides built-in support for non-blocking I/O operations. Threads can also lead to issues like deadlocks, race conditions, or increased memory usage due to GIL (Global Interpreter Lock).

  1. What is the Global Interpreter Lock (GIL), and how does it affect Python's concurrency?

The Global Interpreter Lock is a mechanism in CPython that prevents multiple native threads from executing Python bytecode simultaneously. This means that only one thread can execute Python code at a time, although multiple threads can perform I/O operations concurrently. Asynchronous programming in Python helps mitigate the impact of GIL by allowing non-blocking I/O operations and efficient use of resources.

  1. What is the difference between a coroutine and a generator?

A coroutine is a special type of function that can be paused and resumed using the yield keyword, while a generator is a regular function with a yield statement that produces a sequence of values. Coroutines are used for concurrent programming in Python, while generators are primarily used for iterating over large or infinite data sets efficiently.

  1. What is an event loop, and how does it work in asyncio?

An event loop is the core component of Python's asyncio library that manages the execution of coroutines. It maintains a queue of tasks to be run and continuously polls for new I/O events or timeouts. When a coroutine is yielded back to the event loop, it's added to the task queue, allowing other coroutines to run concurrently. The event loop runs until there are no more tasks in the queue or an exception occurs.

  1. What is the best way to structure my asynchronous code for readability and maintainability?

To structure your asynchronous code effectively, follow these best practices:

  • Use clear and descriptive function names
  • Keep functions short and focused on a single task or concept
  • Use async def for all top-level functions (including main)
  • Use await consistently when calling asynchronous functions
  • Handle exceptions properly using try and except blocks
  • Consider using async context managers to manage shared resources safely
  • Organize your code into logical modules or packages for easier maintenance.
Async Mistakes (Python Programming) | Python | XQA Learn