Asynchronous generators and other objects with an aclose() method may hold connections, cursors, locks, or pending finally blocks that must run when consumption ends. If a loop exits early with break, return, cancellation, or an exception, relying on garbage collection may delay cleanup or run it outside the intended asynchronous context. contextlib.aclosing() turns such an object into an async context manager that awaits object.aclose() on exit.
This guide explains deterministic asynchronous-generator cleanup, early exits, context variables, exceptions, cancellation, differences from closing, integration with HTTP streams and database cursors, ownership, AsyncExitStack, and when a resource’s native async context manager should be preferred.
Your first aclosing block
from contextlib import aclosing
async with aclosing(async_generator()) as values:
async for value in values:
print(value)
When the block exits, aclosing awaits values.aclose(). This happens after normal completion and during exception unwinding.
Early exit with break
async with aclosing(stream()) as items:
async for item in items:
if item.ready:
break
Without explicit closing, the generator may remain suspended. With aclosing, its asynchronous finally blocks run before control leaves the context.
A generator with asynchronous cleanup
async def stream():
resource = await open_resource()
try:
while True:
item = await resource.read()
if item is None:
return
yield item
finally:
await resource.close()
Calling aclose() requests generator termination and allows the async finally block to be awaited.
Why execution context matters
Cleanup may depend on context variables, the current event loop, task identity, tracing spans, or exception state. Closing inside the same async with keeps those dependencies predictable.
Conceptual implementation
from contextlib import asynccontextmanager
@asynccontextmanager
async def manual_aclosing(obj):
try:
yield obj
finally:
await obj.aclose()
The standard helper expresses this pattern consistently and removes repeated boilerplate.
Difference from closing
from contextlib import closing
with closing(resource) as value:
use(value)
closing() calls a synchronous close(). aclosing() awaits an asynchronous aclose(). Using the synchronous helper with a coroutine leaves cleanup unawaited.
The object must provide aclose
aclosing does not require formal inheritance. If the object has no suitable aclose(), context exit fails. Public APIs can describe the expectation with a Protocol:
from typing import Protocol
class AsyncClosable(Protocol):
async def aclose(self) -> None: ...
Prefer a native async context manager
If the resource already implements __aenter__ and __aexit__, use it directly:
async with client.stream() as response:
...
The native context manager may perform setup and teardown steps beyond aclose. aclosing is appropriate when aclose() is the actual complete release contract.
HTTP clients and response streams
HTTP libraries differ. Some responses expose aclose; others provide a dedicated context manager that returns connections to a pool, drains bodies, or updates metrics. Follow the library contract rather than wrapping every asynchronous object blindly.
Asynchronous database cursors
async with aclosing(cursor) as rows:
async for row in rows:
if matches(row):
return row
Even with an early return, the cursor closes before the function returns, reducing leaked server cursors, connections, and locks.
Exceptions during iteration
async with aclosing(stream()) as items:
async for item in items:
process(item) # may raise
The context manager awaits aclose during unwinding. If cleanup also raises, normal exception chaining rules apply. Preserve the original failure and report both causes clearly.
Cancellation
A task may be cancelled during iteration or while awaiting aclose. Cleanup should be short, idempotent, and cancellation-aware. Critical release may need a narrowly protected section provided by the asynchronous framework, but cleanup must not block cancellation indefinitely.
Cleanup timeouts
If aclose may hang on network I/O, a custom context manager can wrap it in a timeout:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def timed_aclosing(obj, seconds):
try:
yield obj
finally:
async with asyncio.timeout(seconds):
await obj.aclose()
Choose a policy for timeout failures and record the resource state for diagnostics.
Idempotency
Ideally, aclose() tolerates repeated calls or clearly reports an already-closed state. aclosing invokes it once for each context entry, but another owner may also attempt cleanup if ownership is unclear.
Do not reuse a closed stream
An asynchronous generator that has been closed will not produce values again. Treat the block as the complete lifetime of that stream and create a new object for another pass.
Ownership
The component that creates a resource normally owns its cleanup. Do not wrap a borrowed object in aclosing when another component plans to continue using it. Public functions should state whether they consume and close the supplied stream.
Factories clarify ownership
async def consume(factory):
resource = factory()
async with aclosing(resource) as items:
async for item in items:
...
Receiving a factory makes it clear that the function creates a fresh resource and owns its lifetime.
Combining with AsyncExitStack
from contextlib import AsyncExitStack, aclosing
async with AsyncExitStack() as stack:
stream_a = await stack.enter_async_context(aclosing(create_a()))
stream_b = await stack.enter_async_context(aclosing(create_b()))
...
AsyncExitStack manages a dynamic number of asynchronous resources and closes them in reverse registration order.
Wrapping domain acquisition
from contextlib import asynccontextmanager, aclosing
@asynccontextmanager
async def service_rows():
async with aclosing(create_stream()) as stream:
yield stream
Callers receive a domain-specific context manager and do not need to know the underlying object uses aclose.
Context variables
One important property is that finalization occurs in the same context as iteration. A generator’s finally block may read contextvars for tracing, tenant identity, locale, or temporary credentials.
Multiple nested streams
Give every independently owned stream its own aclosing block or register it with AsyncExitStack. Do not assume leaving the function will close all suspended generators promptly.
Partially consumed generators
The most important use case is partial consumption. When iteration reaches natural exhaustion, the generator has already completed, but aclosing gives every code path the same deterministic lifetime policy.
Objects other than generators
Any object with a suitable asynchronous aclose() can be wrapped, including channels, sessions, and lightweight wrappers. Confirm that aclose represents the complete cleanup contract.
Testing cleanup
class FakeStream:
def __init__(self):
self.closed = False
def __aiter__(self):
return self
async def __anext__(self):
raise StopAsyncIteration
async def aclose(self):
self.closed = True
Test normal exhaustion, break, return, consumer exceptions, cleanup exceptions, and cancellation. Verify that closure happens before the outer function completes.
Logging and observability
Measure open-stream counts, close duration, timeouts, and cancellation outcomes. Avoid logging response bodies, query values, tokens, or full resource representations.
Common mistakes
- Using closing with aclose: the coroutine is not awaited.
- Wrapping an object with a richer native context manager: teardown steps may be skipped.
- Closing a borrowed resource: establish ownership.
- Relying on generator garbage collection: finally may run late.
- Ignoring cancellation during cleanup: resources may remain half-closed.
- Reusing the closed stream: its lifetime has ended.
Complete early-search example
from contextlib import aclosing
async def find_first(factory, predicate):
async with aclosing(factory()) as stream:
async for item in stream:
if predicate(item):
return item
return None
The function returns as soon as a match is found, but the generator closes before the coroutine hands the result to its caller.
Conclusion
contextlib.aclosing() provides deterministic cleanup for objects with aclose(), especially partially consumed asynchronous generators. It keeps cleanup in the same execution context and makes break, return, cancellation, and exceptions safer.
The official Python contextlib.aclosing documentation defines the helper. Prefer a resource’s native async context manager when available and use aclosing when aclose is the complete release interface.







