Back to Python
2026-04-049 min read

A conceptual overview part 2: the nuts and bolts (Python Programming)

Learn A conceptual overview part 2: the nuts and bolts (Python Programming) step by step with clear examples and exercises.

Title: Mastering Asyncio in Python - A full guide (Part 2: The Nuts and Bolts)

Why This Matters

In today's fast-paced world, efficient and responsive applications are crucial. Asynchronous programming allows us to handle multiple tasks concurrently without blocking the main thread, improving overall performance and user experience. Python's asyncio library is a powerful tool for implementing asynchronous code in your projects. This guide aims to provide you with a deep understanding of asyncio's inner workings, helping you write efficient and effective asynchronous functions and coroutines.

Prerequisites

Before diving into the details of asyncio, it is essential to have a good grasp of Python programming concepts such as functions, classes, and object-oriented programming (OOP). Familiarity with synchronous programming will also be helpful in understanding how asynchronous code differs. Additionally, understanding the basics of multi-threading can help you appreciate the benefits of asyncio for I/O-bound tasks.

Furthermore, it is important to understand the following concepts:

  • Understanding the difference between a function and a coroutine
  • Basic exception handling in Python
  • The concept of cooperative multitasking

Core Concept

The event loop

The event loop is the heart of asyncio. It manages a collection of tasks, scheduling them to run when their turn comes. When a task is ready to run, it takes control from the event loop and executes until it pauses or completes. Once that happens, the event loop selects another task to run. The event loop continues this cycle indefinitely, ensuring that all tasks are executed in an orderly fashion.

Event Loop Scheduler

The event loop's scheduler is responsible for deciding which tasks should be run next based on their readiness and priority. It uses a queue to store the tasks waiting to be executed. The scheduler ensures that the event loop runs the most suitable task at any given time, optimizing the performance of your asynchronous code.

Coroutines and await

Coroutines are special functions that can be paused and resumed at specific points during execution. In asyncio, coroutines are used for asynchronous operations. The async def keyword is used to declare a coroutine function. To pause a coroutine, we use the await keyword followed by an expression representing the asynchronous operation we want to perform.

Cooperative Multitasking

Coroutines in asyncio use cooperative multitasking, meaning they voluntarily yield control to other coroutines when necessary. This approach allows the event loop to manage multiple tasks concurrently without the overhead of context switching between threads or processes.

Futures

A Future represents the result of an asynchronous operation that hasn't completed yet. It can be either a Promise (a Future that will eventually produce a value) or a CancelledFuture (a Future that has been canceled). In asyncio, Futures are used to manage the results of asynchronous operations and handle exceptions that may occur during their execution.

Running Coroutines with create_task()

The event loop's create_task() function can be used to run a coroutine as a task. The returned Task object represents the coroutine's execution and can be awaited or canceled.

A homemade asyncio.sleep

Creating an asynchronous version of a blocking operation like time.sleep() can be done using Futures and the event loop's create_task() function. Here's an example of how to create a custom async sleep function:

import asyncio

async def sleep(seconds):
task = asyncio.Task(asyncio.sleep(seconds))
await task

Worked Example

async def main():

print("Start")

await sleep(3)

print("End")

await asyncio.sleep(1)

print("Finished sleeping for 1 second")

if __name__ == "__main__":

asyncio.run(main())


In this example, we create a coroutine `sleep()` that accepts the number of seconds to sleep as an argument. Inside the coroutine, we create a Task representing the sleep operation and then await it, effectively suspending the execution of the coroutine for the specified duration. The main function demonstrates how to use our custom async sleep function.

Worked Example

In this example, we'll create an asynchronous web scraper that fetches the titles of the top 10 articles from a news website and prints them. We will use the aiohttp library for HTTP requests and BeautifulSoup for parsing the HTML content.

import asyncio
import aiohttp
from bs4 import BeautifulSoup

async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
return None
soup = BeautifulSoup(await response.text(), 'html.parser')
return soup.find('div', {'class': 'top-headlines'})

async def process_articles(soup):
articles = soup.find_all('article')
for article in articles:
title = article.h2.a['href']
print(title)

async def main():
url = 'https://newsapi.org/v2/top-headlines?country=us&category=technology&apiKey=YOUR_API_KEY'
soup = await fetch(url)
if soup:
await process_articles(soup)

if __name__ == "__main__":
asyncio.run(main())

In this example, we define three coroutines: fetch(), process_articles(), and main(). The fetch() function fetches the HTML content of a given URL using the aiohttp library and returns a BeautifulSoup object containing the top-headlines div. The process_articles() function extracts the titles from the soup object and prints them. The main() function coordinates these coroutines, fetching the HTML content and processing the articles asynchronously.

Common Mistakes

  1. ### Forgetting to await a coroutine

When calling a coroutine, it's essential to await it to ensure that the event loop schedules its execution. Failing to do so will result in a deadlock, where the coroutine never gets executed and blocks the event loop.

  1. ### Not handling exceptions properly

Asynchronous code can lead to complex exception handling scenarios. Make sure to handle exceptions appropriately and propagate them if necessary. It's important to remember that exceptions in asyncio are handled at the coroutine level, not the task level.

  1. ### Misusing Futures

Futures should be used to manage the results of asynchronous operations and handle exceptions. Avoid using them for synchronous operations or as a general-purpose container for data. Instead, use synchronous functions for such tasks.

  1. ### Ignoring the event loop's lifecycle

The event loop has a lifecycle that includes creation, running, and termination. It's essential to understand this lifecycle and manage it properly, especially when working with long-running tasks or dealing with exceptions.

Subheadings under Common Mistakes:

  • Forgetting to close resources (e.g., sockets) in finally blocks
  • Not properly canceling tasks that take too long to complete
  • Not using async context managers for acquiring and releasing resources

Practice Questions

  1. Write an async function that fetches the content of a given URL using aiohttp and returns it as a string.
  2. Implement an asynchronous version of the os.listdir() function using asyncio and aiofiles.
  3. Create an asynchronous web scraper that fetches the top 10 articles from a news website and saves them to a file.
  4. Write an asynchronous function that downloads multiple files concurrently using aiohttp and aiofiles.
  5. Implement an asynchronous chat server using asyncio, sockets, and coroutines for handling client connections.
  6. Write an asynchronous function to read a large file line by line without blocking the event loop.
  7. Create an asynchronous function that performs a long-running computation and periodically updates a UI element using a GUI library like Tkinter or PyQt.
  8. Implement an asynchronous function that fetches data from multiple APIs concurrently, processes the results, and returns the aggregated result.
  9. Write an asynchronous function to download and save multiple images from a website using aiohttp and Pillow.
  10. Create an asynchronous web scraper that fetches data from multiple pages of a website and saves it to a database.

FAQ

### Can I use asyncio for CPU-bound tasks?

Yes, but it may not provide significant performance improvements compared to synchronous code for CPU-bound tasks. Asyncio is particularly useful for I/O-bound tasks that can benefit from non-blocking execution. However, you can still use asyncio for CPU-bound tasks by breaking them into smaller chunks and using asyncio.gather() or other techniques to execute them concurrently.

### How do I cancel a running task in asyncio?

You can cancel a Task using the cancel() method. If the task has not yet started, it will not be scheduled. If it's running or paused, it will be interrupted and its Future will be marked as CancelledFuture. However, if the task is already completed, cancellation will have no effect.

### What happens when an exception occurs in an awaited coroutine?

When an exception occurs in an awaited coroutine, the event loop catches it and propagates it to the calling coroutine or the main function. If not handled, the program will terminate with an error message. It's essential to handle exceptions appropriately to ensure your asynchronous code runs smoothly.

### How can I run multiple tasks concurrently in asyncio?

You can use asyncio.gather() to run multiple coroutines concurrently. This function returns a Future that resolves when all the given coroutines have completed, allowing you to handle their results or exceptions collectively.

### What is the difference between an async function and a regular function in Python?

An async function is a special type of function that can contain await expressions, making it capable of suspending its execution and waiting for other asynchronous operations to complete. A regular function, on the other hand, runs synchronously and does not support awaiting other coroutines or asynchronous operations.

### How do I create a custom scheduler for asyncio?

Creating a custom scheduler for asyncio involves implementing the AbstractEventLoop class from the asyncio library and overriding its methods to suit your specific needs. This can be useful when working with specialized hardware or custom networking protocols that require a tailored event loop implementation.

### How do I test asynchronous code in Python?

Testing asynchronous code can be challenging due to the non-deterministic nature of asyncio. However, libraries like unittest.mock and pytest-asyncio can help simplify testing by providing tools for mocking coroutines and managing test fixtures. Additionally, using a test-driven development (TDD) approach can help ensure that your asynchronous code is robust and reliable.

### What are some best practices for writing asynchronous code in Python?

Some best practices for writing asynchronous code in Python include:

  • Keeping coroutines short and focused on a single task or operation
  • Using asyncio.gather() to run multiple tasks concurrently when appropriate
  • Avoiding deep nesting of await expressions and using try-except blocks to handle exceptions
  • Closing resources (e.g., sockets) in finally blocks or using async context managers
  • Profiling your code to identify bottlenecks and optimize performance
  • Writing clean, modular, and testable code that follows the principles of object-oriented programming (OOP)

### How can I measure the performance of my asynchronous code?

Measuring the performance of your asynchronous code can be done using various tools like cProfile, line_profiler, and timeit. These tools provide insights into the execution time, memory usage, and function calls in your code, helping you identify bottlenecks and optimize performance. Additionally, using a benchmarking approach can help ensure that improvements are consistent across different scenarios and hardware configurations.

### How do I handle long-running tasks in asyncio?

Long-running tasks can be handled in asyncio by breaking them into smaller chunks or using techniques like asyncio.sleep() to control the execution pace. Additionally, you can use a background task queue like aioredis, RQ, or Celery to offload long-running tasks and keep the main event loop responsive.

### What are some common libraries for working with asynchronous I/O in Python?

Some common libraries for working with asynchronous I/O in Python include:

  • aiohttp: A popular library for building high-performance HTTP clients and servers
  • asyncio: The built-in library for implementing asynchronous code in Python
  • aioredis: An efficient Redis client for asynchronous operations

-

A conceptual overview part 2: the nuts and bolts (Python Programming) | Python | XQA Learn