DEV Community

Cover image for Why You Should Care About Async Context Managers and Iterators
Sushant Gaurav
Sushant Gaurav

Posted on

Why You Should Care About Async Context Managers and Iterators

Async Context Managers (async with) and Async Iterators (async for)

A context manager in Python is a special object that manages the setup and cleanup of resources automatically when the with statement is used. It ensures that resources like files, network connections, or locks are properly acquired and released, even if an error occurs during use.

A context manager defines two methods:

  • __enter__(): It runs when entering the with block. It is typically used to set up the resource.
  • __exit__(exc_type, exc_val, exc_tb): It runs when leaving the with block. It usually handles cleanup, like closing files or releasing locks.

Asynchronous Context Managers (async with)

It is like a regular context manager but supports asynchronous setup and teardown. It is ideal for managing resources in async code (like connections, files, locks).

  • It implements two special coroutine methods:
    • __aenter__() — awaited when entering the context
    • __aexit__() — awaited when exiting the context, even if exceptions occur

Example:

import asyncio
class AsyncContextManager:
    async def __aenter__(self):
        print("Entering async context")
        # for example, open an async connection
        return self
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # for example, clean up async connection
        print("Exiting async context")

async def main():
    async with AsyncContextManager():
        print("Inside async context")

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

Output:

Entering async context
Inside async context
Exiting async context
Enter fullscreen mode Exit fullscreen mode

Note: async with automatically awaits __aenter__ and __aexit__ coroutines.

Asynchronous Iterators (async for)

It is an object from which can be async for to receive values asynchronously.

  • It must implement:
    • __aiter__() for returning the async iterator object (usually self)
    • __anext__() for returning an awaitable that produces the next item or raises StopAsyncIteration

It is useful in cases where there is a need to iterate over asynchronous streams of data — for example, reading from async sockets, files, or APIs streaming data. It allows handling each item as it arrives without blocking the event loop.

Example:

import asyncio

class AsyncCounter:
    def __init__(self, max):
        self.max = max
        self.current = 0
    def __aiter__(self):
        return self
    async def __anext__(self):
        if self.current >= self.max:
            raise StopAsyncIteration
        await asyncio.sleep(0.5)  # simulate async wait
        self.current += 1
        return self.current

async def main():
    async for number in AsyncCounter(3):
        print(f"Got number: {number}")

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

Output:

Got number: 1
Got number: 2
Got number: 3
Enter fullscreen mode Exit fullscreen mode

Integrating asyncio with External Libraries

When working with real-world async programs, there is a need to interact with external resources like HTTP APIs or databases. Libraries such as aiohttp (for HTTP) and aiomysql (for MySQL) wrap these services so they work seamlessly with Python's asyncio event loop.

Asynchronous HTTP with aiohttp

aiohttp allows sending HTTP requests using async/await. It allows working with many URLs at once.

Example:

import asyncio
import aiohttp

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

async def main():
    urls = [
        'https://example.com',
        'https://httpbin.org/get',
        'https://python.org'
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        pages = await asyncio.gather(*tasks)  # Fetch all pages concurrently
        for i, html in enumerate(pages):
            print(f"Page {i+1} length: {len(html)}")

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

Here,

  • The ClientSession manages connections efficiently.
  • Each call to fetch_url is a coroutine, run concurrently.

Asynchronous Databases with aiomysql

aiomysql is an async driver for MySQL databases. It is built for asyncio.

Example:

import asyncio
import aiomysql

async def get_data(pool):
    async with pool.acquire() as connection:
        async with connection.cursor() as cursor:
            await cursor.execute('SELECT some_column FROM some_table;')
            print(await cursor.fetchall())

async def main():
    pool = await aiomysql.create_pool(
        host='127.0.0.1', port=3306,
        user='root', password='pass', db='testdb'
    )
    tasks = [get_data(pool) for _ in range(10)]  # Launch 10 queries concurrently
    await asyncio.gather(*tasks)
    pool.close()
    await pool.wait_closed()

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

Here,

  • Each get_data coroutine performs its own database query. They all run in parallel inside the event loop.
  • Connection pooling via aiomysql.create_pool is recommended for efficiency.

Integrating with Web Frameworks

Many frameworks now support async views. For example:

  • Django: It supports async views since version 3.1.

    async def async_view(request):
        await asyncio.sleep(1)
        return JsonResponse({'message': 'Hello, Async Django!'})
    
  • Quart: Flask-inspired, built for async:

    @app.route('/')
    async def index():
        await asyncio.sleep(1)
        return 'Hello, Quart!'
    

What Changed in Python 3.11+ for asyncio

Python 3.11 brought several improvements and new features to the asyncio module:

  • Task Groups:
    • Introduced asyncio.TaskGroup for easier and safer management of concurrent tasks.
    • Task groups allow launching and monitoring multiple async tasks in a block. If any task fails, exceptions are grouped and managed together.
  • Exception Groups:
    • New ExceptionGroup type handles multiple exceptions for concurrent async code. It is useful when several tasks are done at once.
  • Improved Tracebacks and Speed:
    • Error messages and tracebacks in async code are clearer and provide better context.
    • Python 3.11 is measurably faster than previous releases.
  • Breaking Changes:
    • Legacy features removed. For instance, generator-based coroutines via asyncio.coroutine are now fully deprecated and removed. async def should always be used.
    • Some APIs (like asyncio.start_server) return different objects, so always review the documentation if upgrading projects.
  • Other Improvements:
    • New or improved timeout handling (asyncio.timeout), better audit events, TLS support in streams, and more.

await vs. asyncio.run() — Key Differences

asyncio.run()

  • Purpose: It is used to start the event loop and run a top-level async function (coroutine) from regular (sync) code.
  • Where Used: Only at the very top level, i.e. outside any async def functions.
  • Behaviour: It initialises the async event loop, executes the coroutine until it is complete, then shuts down the loop. It can only be called once per program, typically.

Example:

async def main():
    ...
asyncio.run(main())  # entry point from normal script
Enter fullscreen mode Exit fullscreen mode

await keyword

  • Purpose: It suspends (pauses) an async function until the "awaitable" finishes. It lets other tasks run while waiting.
  • Where Used: It is only used inside an async def function. await cannot be directly used in regular, non-async code.
  • Behaviour: It waits for the result of an "awaitable" (another coroutine, a Task, or a Future). The event loop continues running other scheduled tasks meanwhile.

Example:

async def fun():
    result = await asyncio.sleep(1)
    print("done sleeping")
Enter fullscreen mode Exit fullscreen mode

Thanks for reading!

We have covered a lot in this article and our article series. In the upcoming article, we will go deeper into asyncio's Interview Questions and practice some of its commonly asked and used Problems.

Stay tuned to unlock the full power of Python’s asynchronous programming!

Top comments (0)