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 thewithblock. 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())
Output:
Entering async context
Inside async context
Exiting async context
Note:
async withautomatically 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 raisesStopAsyncIteration
-
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())
Output:
Got number: 1
Got number: 2
Got number: 3
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())
Here,
- The
ClientSessionmanages connections efficiently. - Each call to
fetch_urlis 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())
Here,
- Each
get_datacoroutine performs its own database query. They all run in parallel inside the event loop. - Connection pooling via
aiomysql.create_poolis 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.TaskGroupfor 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.
- Introduced
-
Exception Groups:
- New
ExceptionGrouptype handles multiple exceptions for concurrent async code. It is useful when several tasks are done at once.
- New
-
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.coroutineare now fully deprecated and removed.async defshould always be used. - Some APIs (like
asyncio.start_server) return different objects, so always review the documentation if upgrading projects.
- Legacy features removed. For instance, generator-based coroutines via
-
Other Improvements:
- New or improved timeout handling (
asyncio.timeout), better audit events, TLS support in streams, and more.
- New or improved timeout handling (
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 deffunctions. - 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
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 deffunction.awaitcannot 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")
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)