DEV Community

Cover image for Python Asyncio Explained: How Asynchronous Programming Works in Python
David Fagbuyiro
David Fagbuyiro

Posted on

Python Asyncio Explained: How Asynchronous Programming Works in Python

Modern applications often need to handle many tasks at the same time. A web server may receive thousands of requests, a scraper may download many pages, or an application may wait for multiple APIs to respond. Running each task one after another can be slow and inefficient.

Python solves many of these problems with asyncio, a built in library for writing asynchronous programs. Instead of blocking the entire application while waiting for operations like network requests or file access, asyncio allows Python to work on other tasks during that waiting time.

This article explains what asyncio is, how it works, and when you should use it.

What Is Asyncio in Python?

asyncio is Python’s standard library for writing concurrent code using the async and await syntax. It provides tools for running multiple tasks, managing network operations, handling subprocesses, and coordinating asynchronous workflows.

The main idea behind asyncio is simple:

Do not wait when you do not have to.

For example, imagine a program that downloads data from three websites.

A normal Python program might:

Request data from website A.
Wait for the response.
Request data from website B.
Wait for the response.
Request data from website C.

The program spends most of its time waiting.

With asyncio, the program can start all three requests and switch between them while waiting for responses.

This does not mean Python is running three pieces of code at exactly the same time. Instead, asyncio uses concurrency by quickly switching between tasks when they are waiting.

Understanding Synchronous vs Asynchronous Code

Traditional Python code is usually synchronous.

Example:

import time

def download_file(name):
    print(f"Downloading {name}")
    time.sleep(3)
    print(f"{name} completed")

download_file("file1")
download_file("file2")
Enter fullscreen mode Exit fullscreen mode

The second download cannot start until the first one finishes.

If each task takes three seconds, the total time is around six seconds.

With asynchronous programming:

import asyncio

async def download_file(name):
    print(f"Downloading {name}")
    await asyncio.sleep(3)
    print(f"{name} completed")

async def main():
    await asyncio.gather(
        download_file("file1"),
        download_file("file2")
    )

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Both tasks begin together. While one task is waiting, asyncio can allow another task to continue.

The result is better performance for workloads that spend a lot of time waiting.

How Does Asyncio Work?

The core component behind asyncio is the event loop.

The event loop continuously checks which tasks are ready to run and manages their execution. When a task reaches an operation that requires waiting, it gives control back to the event loop. The loop can then run another task instead.

A simplified process looks like this:

  1. Start the event loop.
  2. Create asynchronous tasks.
  3. Run each task until it reaches an await.
  4. Pause tasks that are waiting.
  5. Continue running tasks that can make progress.
  6. Resume paused tasks when their results are available.

The event loop is what allows asyncio programs to handle many operations efficiently without creating a separate thread for every task.

What Are Coroutines?

A coroutine is a special function that can pause and resume execution.

In Python, coroutines are created using the async def keyword.

Example:

async def hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")
Enter fullscreen mode Exit fullscreen mode

This function does not immediately run when called. Instead, it creates a coroutine object that asyncio can schedule.

The await keyword tells Python:

"Pause this task here until the operation completes, but allow other tasks to run."

Python’s asyncio documentation describes coroutines and tasks as the main building blocks for asynchronous applications.

Tasks and asyncio.gather()

A coroutine becomes useful when it is scheduled as a task.

Tasks allow asyncio to manage multiple coroutines at the same time.

Example:

import asyncio

async def worker(number):
    print(f"Worker {number} started")
    await asyncio.sleep(2)
    print(f"Worker {number} finished")

async def main():
    task1 = asyncio.create_task(worker(1))
    task2 = asyncio.create_task(worker(2))

    await task1
    await task2

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Another common approach is asyncio.gather():

await asyncio.gather(
    worker(1),
    worker(2),
    worker(3)
)
Enter fullscreen mode Exit fullscreen mode

gather() runs multiple awaitable objects and waits until they complete.

Asyncio Does Not Make Everything Faster

A common misunderstanding is that asyncio automatically speeds up every Python program.

It does not.

Asyncio works best for I/O-bound tasks, where the program spends time waiting.

Examples:

  • API requests
  • Web scraping
  • Database queries
  • Network servers
  • Reading from external services

The Python documentation specifically notes that asyncio is often a good fit for high level network code and I/O-bound workloads.

However, asyncio is usually not the best choice for CPU-heavy tasks.

Examples:

  • Video processing
  • Machine learning calculations
  • Large mathematical operations

For CPU intensive workloads, multiprocessing or optimized libraries are often better choices.

Asyncio vs Threads

Both asyncio and threads can handle multiple operations, but they work differently.

Threads use the operating system to run multiple execution paths.

Asyncio usually runs tasks inside a single thread using cooperative scheduling.

Threads:

  • Easier for existing blocking code
  • Can run CPU work in parallel with multiple cores
  • Require managing shared resources carefully

Asyncio:

  • Uses fewer resources
  • Works well with many network connections
  • Requires code designed with async support

For example, a web crawler downloading thousands of pages can often benefit from asyncio because most of the time is spent waiting for responses.

Common Asyncio Mistakes

Blocking the Event Loop

This is one of the biggest mistakes.

Bad:

async def task():
    time.sleep(5)
Enter fullscreen mode Exit fullscreen mode

time.sleep() blocks the entire event loop.

Better:

async def task():
    await asyncio.sleep(5)
Enter fullscreen mode Exit fullscreen mode

The asynchronous version allows other tasks to run.

Using Asyncio With Normal Libraries

Not every Python library supports async operations.

For example, a normal HTTP library may block while waiting for a response. You need libraries designed for asynchronous programming.

Creating Too Many Tasks

Asyncio makes it easy to create thousands of tasks, but unlimited concurrency can overload APIs, databases, or your own system.

Good asynchronous programs still need limits and proper resource management.

When Should You Use Asyncio?

Asyncio is a good choice when your application spends a lot of time waiting.

Common use cases include:

  • Web Scraping A scraper that collects information from many websites can use asyncio to process multiple requests efficiently.
  • APIs and Web Servers Many modern Python web frameworks use asynchronous programming to handle large numbers of connections.
  • Real-Time Applications Chat applications, notifications, and streaming services often benefit from asynchronous processing.
  • Database Operations Applications that perform many database queries can reduce idle waiting time with asynchronous database drivers.

Conclusion

Python asyncio provides a powerful way to write programs that handle many tasks efficiently. By using coroutines, tasks, and an event loop, asyncio allows applications to continue working while waiting for slow operations to complete.

The biggest advantage of asyncio is not making Python run faster. It is making better use of time that would otherwise be spent waiting.

For network applications, APIs, web scraping, and other I/O-heavy workloads, understanding asyncio can help developers build faster and more scalable Python applications. The official asyncio documentation remains the best reference for learning the available APIs and patterns. Python asyncio documentation

Top comments (1)

Collapse
 
lunarose profile image
Luna Rose

This is such a clear, beginner-friendly breakdown of asyncio. I really appreciate how you highlight the critical distinction: asyncio shines for I/O-bound waiting workloads, but it’s not a magic speed boost for CPU-heavy tasks. The synchronous vs async code examples and the common pitfalls like blocking the event loop are super practical reminders for every Python developer.