← Back to Blogs

Demystifying Python asyncio: A Practical Guide

By CoilPad • January 3, 2026 · 12 min read

Asynchronous programming in Python has evolved significantly over the years. With the introduction of the asyncio library in Python 3.4 and the async/await syntax in 3.5, writing concurrent code has become much more accessible.

However, it still confuses many developers. Terms like "event loop," "coroutines," and "futures" can be overwhelming. Let's break it down into practical concepts.

Sync vs Async: The Analogy

Imagine you are cooking dinner.

  • Synchronous (Blocking): You put water on to boil and stare at the pot for 10 minutes until it's done. You do nothing else. Then you chop vegetables.
  • Asynchronous (Non-blocking): You put water on to boil. While waiting, you chop vegetables. When the water boils, you switch back to handling the pasta.

The Basics: async and await

To define a function that can be paused and resumed (a coroutine), you use async def. To call it and wait for the result without blocking the whole program, you use await.

import asyncio

async def say_hello():
    print("Hello...")
    # Simulate an I/O operation (like a network request)
    await asyncio.sleep(1)
    print("...World!")

# You can't just call say_hello() directly
# You need to run it in an event loop
if __name__ == "__main__":
    asyncio.run(say_hello())

Running Things concurrently

The real power comes when you run multiple things at once.asyncio.gather is your best friend here.

import asyncio
import time

async def brew_coffee():
    print("Starting coffee...")
    await asyncio.sleep(2)
    print("Coffee is ready!")
    return "Coffee"

async def toast_bread():
    print("Starting toast...")
    await asyncio.sleep(1)
    print("Toast is ready!")
    return "Toast"

async def breakfast():
    start = time.perf_counter()
    
    # Run both at the same time!
    # The total time will be roughly the max structure(max(2, 1)) = 2 seconds
    # Instead of sum(2, 1) = 3 seconds
    results = await asyncio.gather(brew_coffee(), toast_bread())
    
    end = time.perf_counter()
    print(f"Finished in {end - start:.2f} seconds")
    print(f"Result: {results}")

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

Common Pitfall: Blocking the Loop

One of the most common mistakes is using blocking calls inside an async function. If you use time.sleep(5) orrequests.get(), you pause the ENTIRE event loop. Nothing else can run.

import time
import asyncio

async def bad_coroutine():
    # This BLOCKS everything!
    time.sleep(5) 
    print("Done sleeping")

async def good_coroutine():
    # This yields control back to the loop
    await asyncio.sleep(5)
    print("Done sleeping")

When to use asyncio?

Asyncio shines for IO-bound tasks (network requests, DB queries, reading files). For CPU-bound tasks (heavy math, image processing), asyncio won't help you—use multiprocessing instead.

Try it yourself

Asyncio is a massive topic, but starting with run(), gather(), and understanding non-blocking I/O is 80% of what you need for daily tasks.

Write Async Code Faster

CoilPad is the perfect sandbox for testing asyncio snippets. No setup required—just type run run.

Download CoilPad