Skip to main content

Command Palette

Search for a command to run...

Understanding Async Programming in Python: asyncio Explained

Published
4 min readView as Markdown

Unlock the power of Python asyncio to write efficient, non-blocking code. This asyncio tutorial covers async programming Python developers need to get started today.

Asynchronous programming has become a critical skill for Python developers aiming to build high-performance applications. If you've ever wondered how to effectively handle I/O-bound tasks without blocking your program, then Python asyncio is the toolkit you need. In this asyncio tutorial, we'll break down the core concepts of asynchronous Python programming and provide practical examples to help you get started.

What Is Async Programming Python Developers Should Know?

Traditional Python code executes sequentially—one task after another. While this is simple to understand, it often leads to inefficiencies, especially when dealing with I/O operations like network requests or file handling. These operations can block your program, causing delays.

Async programming Python addresses this by allowing your program to perform multiple tasks seemingly at the same time. It does this by not waiting for I/O operations to complete before moving on to other tasks. This approach improves resource utilization and responsiveness.

Why Use Python asyncio?

asyncio is Python’s built-in library for writing asynchronous code using the async/await syntax. It provides:

  • Event loop management: Handles and schedules asynchronous tasks.
  • Coroutines: Special functions that pause and resume their execution.
  • Futures and Tasks: Represent ongoing work and manage execution.
  • Non-blocking I/O support: Enables network, file, and other operations to run without blocking.

By using asyncio, developers can write concurrent code that scales well without the complexity of threads or processes.

Getting Started: Basic Asyncio Tutorial

Let’s jump into an example to understand how asyncio works in practice.

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World!")

async def main():
    # Schedule two coroutines to run concurrently
    await asyncio.gather(say_hello(), say_hello())

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

Explanation:

  • async def defines a coroutine.
  • await pauses the coroutine until the awaited task completes.
  • asyncio.sleep(1) simulates a non-blocking delay.
  • asyncio.gather() runs multiple coroutines concurrently.
  • asyncio.run() starts the event loop and runs the main coroutine.

Here, both say_hello() coroutines run concurrently, so the total runtime is about 1 second instead of 2 seconds if run sequentially.

Understanding the Event Loop in Asynchronous Python

At the heart of asyncio is the event loop, which continuously checks for and executes tasks that are ready to run. You can think of it as the manager coordinating multiple coroutines.

You typically don't need to interact directly with the event loop, but advanced use cases might require it:

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

However, in Python 3.7+, asyncio.run() is preferred for simplicity and safety.

Asyncio Patterns for Real-World Applications

1. Handling Network Requests Concurrently

Using asyncio with libraries like aiohttp allows you to fetch multiple URLs simultaneously:

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        "https://example.com",
        "https://python.org",
        "https://gingiris.hashnode.dev"
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        for content in results:
            print(f"Downloaded {len(content)} characters")

asyncio.run(main())

2. Scheduling Periodic Tasks

async def periodic():
    while True:
        print("Task running...")
        await asyncio.sleep(5)

asyncio.run(periodic())

Common Pitfalls to Avoid in Asynchronous Python

  • Blocking calls inside async code: Avoid using regular blocking functions (e.g., time.sleep()) inside coroutines.
  • Not awaiting coroutines: Forgetting to use await can lead to unexpected behavior.
  • Mixing threading and asyncio carelessly: While possible, it requires careful design.

Where to Go Next: Advanced Asyncio Features

Once you’re comfortable with the basics, explore:

  • asyncio.Queue for producer-consumer patterns.
  • Synchronization primitives like asyncio.Lock and asyncio.Event.
  • Creating custom event loops or integrating with other async frameworks.

Explore Gingiris Open-Source Async Projects

If you want to see async programming Python in action, the Gingiris GitHub organization hosts a variety of open-source projects where asyncio plays a key role. Diving into real codebases is a fantastic way to deepen your understanding and contribute to the community.


Asyncio unlocks Python’s potential for concurrent programming in a clean and efficient way. Start experimenting with the examples above and explore the rich ecosystem around asynchronous Python. For continuous learning, keep an eye on Gingiris projects and share your own async adventures!

Ready to level up your async skills? Check out Gingiris GitHub and contribute to open-source projects today!

K

The event loop explanation here is clearer than most asyncio tutorials I have read. What clicked for me is your framing of coroutines as "pause and resume" — that mental model makes it obvious why CPU-bound work blocks the loop even inside an async function. We switched our API scraping pipeline from threads to asyncio and the connection management alone dropped our error rate by 40%.

More from this blog

Gingiris Insights

9 posts