contextlib.aclosing: Close Async Generators Safely

Published on: August 30, 2026
Reading time: 5 minutes
Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python nullcontext: Optional Contexts

    Use Python nullcontext to unify optional files, locks, transactions, sessions, and borrowed resources without duplicate branches.

    Ler mais

    Tempo de leitura: 4 minutos
    30/08/2026
    Detailed close-up texture of a snake's patterned skin showcasing natural patterns and scales.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    weakref.finalize: Cleanup Without Retaining Objects

    Learn Python weakref.finalize for safe fallback cleanup without retaining objects, including alive, detach, shutdown, and explicit close.

    Ler mais

    Tempo de leitura: 5 minutos
    30/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    SimpleNamespace: Lightweight Attribute Objects

    Learn Python SimpleNamespace for lightweight attribute objects, dictionary conversion, copying, JSON, and choosing better typed models.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ChainMap: Layered Mappings

    Learn Python ChainMap for layered configuration and scopes, including precedence, first-map writes, snapshots, and safe mutation.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed close-up of yellow and white albino python scales, capturing texture and pattern.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    itertools.pairwise: Analyze Consecutive Pairs

    Learn Python itertools.pairwise to analyze consecutive pairs, calculate deltas, detect transitions, gaps, and ordering problems.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    itertools.batched: Process Iterables in Batches

    Learn Python itertools.batched to process iterables in chunks, control memory, use strict mode, and build resilient data pipelines.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026