Welcome to part three of our Python 3 features series! Today, we’re digging into asyncio and the async/await syntax that make asynchronous code feel right at home in Python.

When you print() in Python, it waits until it’s done before moving on. But what if you want to do multiple things at once, like fetch data from the web, read files, and process user input, all without blocking? Enter asyncio.

1. The Event Loop Basics

asyncio (added in Python 3.4 via PEP 3156) revolves around an event loop, a little manager that runs your tasks and switches between them when they’re waiting.

import asyncio

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

# Old-school way on 3.4:
loop = asyncio.get_event_loop()
loop.run_until_complete(say_after(1, 'hello'))
loop.close()

Here, asyncio.sleep() is non-blocking, you give control back to the loop, which can run other tasks in the meantime.

2. Running Multiple Tasks

Let’s run two coroutines side by side:

import asyncio

async def main():
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    print('started at', asyncio.get_event_loop().time())
    await task1
    await task2
    print('finished at', asyncio.get_event_loop().time())

asyncio.run(main())  # New in 3.7: handles loop setup/teardown

You’ll see “hello” after 1 second, “world” after 2 seconds, and timestamps showing overlap.

3. asyncio.run() and Cleanup

Python 3.7 introduced asyncio.run(), so you don’t have to manage the loop yourself. It sets up, runs your main coroutine, and tears down the loop neatly.

asyncio.run(main())  # instead of manually creating and closing the loop

4. await Is Your New Best Friend

In Python 3.5 (PEP 492), the async/await keywords made coroutine code look like regular functions:

async def fetch_data(x):
    print(f'Fetching {x}...')
    await asyncio.sleep(1)
    return f'Data {x}'

async def main():
    results = await asyncio.gather(
        fetch_data('A'),
        fetch_data('B'),
        fetch_data('C'),
    )
    print(results)

asyncio.run(main())

asyncio.gather() runs coroutines in parallel and collects their results.

5. When to Use Asyncio

  • IO-bound tasks: network requests, file reads/writes, database calls
  • High concurrency: thousands of small tasks, websockets, chat servers

Don’t use it for CPU-heavy work, that still needs threads or processes.


That’s a quick tour of asyncio and the modern async/await syntax. Next up, we’ll talk about type hints and variable annotations to make your code easier to understand and maintain. Happy coding!