# Async await in Python

July 2, 2026 Python Performance

Python can utilize async/await keywords and event-loop to improve performance of IO-bound tasks. This article walks through the fundamentals.

Installing asyncio

$ pip3 install asyncio

Basic example

import asyncio


async def slow_function(id: str) -> str:
    await asyncio.sleep(1)
    return f"[Done] {id}"


async def main() -> None:
    result = await slow_function("one")
    print(result)


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

Await multiple Futures

# this is effectively synchronous because all Futures are awaited in sequence
async def main() -> None:
    for i in range(10):
        result = await slow_function(i)
        print(result)
# execute and await all at once
async def main() -> None:
    results = await asyncio.gather(*[slow_function(i) for i in range(10)])
    print(results)

Async generators

from typing import AsyncIterable
...

# generate all results in sequence
async def generate_results(limit: int) -> AsyncIterable[str]:
    for i in range(limit):
        result = await slow_function(i)
        yield result


async def main() -> None:
    async for result in generate_results(10):
        print(result)

    # async list comprehension
    results = [result async for result in generate_results(3)]

Convert synchronous functions to async

# sync function e.g. could be sending out requests
def slow_function_sync(id: int) -> str:
    # NOTE: time.sleep block the thread, asyncio.sleep does not.
    asyncio.sleep(0.5)
    return f"[Done] {id}"


# wrapper to convert sync function into async
async def slow_function_async(id: int) -> str:
    return await asyncio.to_thread(slow_function_sync, id)


async def main() -> None:
    for i in range(4):
        # await as async
        result = await slow_function_async(i)
        print(result)

Finding blocking operations

Blocking operations can kill the performance of asyncio coroutines. We can enable debug mode during development to detect blocking operations inside async functions. This will log warnings with names of functions where blocking operations are present.

asyncio.run(main(), debug=True)

Practical Example

import asyncio
from typing import Optional
from pydantic import BaseModel
import httpx
import logging


class UserAddress(BaseModel):
    street: str
    city: str


class User(BaseModel):
    id: int
    name: str
    username: str
    email: str
    address: UserAddress


class UserRepo:
    __base_url = "https://jsonplaceholder.typicode.com/users"

    async def list_users(self) -> list[User]:
        async with httpx.AsyncClient() as client:
            res = await client.get(self.__base_url)
            raw = res.json()
            return [User(**entry) for entry in raw]

    async def find_by_id(self, id: int) -> Optional[User]:
        async with httpx.AsyncClient() as client:
            res = await client.get(f"{self.__base_url}/{id}")
            if res.status_code == 404:
                return None
            raw = res.json()
            return User(**raw)


async def main() -> None:
    # this also enabled httpx request logging.
    logging.basicConfig(level=logging.INFO)
    repo = UserRepo()
    
    # fetch first 10 in parallel.
    results = await asyncio.gather(*[repo.find_by_id(i) for i in range(1, 11)])
    for user in results:
        print(user)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except Exception as ex:
        logging.error(ex)