ExitStack no Python: gerencie recursos

Publicado em: 11/08/2026
Tempo de leitura: 6 minutos
Rede de servidores representando gerenciamento de recursos com ExitStack no Python

Gerenciar vários arquivos, conexões, locks e outros recursos ao mesmo tempo pode deixar o código cheio de blocos with aninhados e difícil de adaptar. O ExitStack no Python, disponível no módulo contextlib, resolve esse problema ao permitir que context managers sejam registrados dinamicamente e encerrados na ordem correta. Ele é especialmente útil quando a quantidade de recursos só é conhecida em tempo de execução.

Neste guia, você aprenderá como funciona o ExitStack, quando ele é melhor que vários blocos with, como abrir arquivos dinamicamente, registrar callbacks de limpeza, transferir a responsabilidade de encerramento, tratar falhas durante a aquisição de recursos e testar fluxos de forma previsível. O tema complementa nossos conteúdos sobre context managers, tempfile, zipfile, filecmp e zoneinfo.

O problema dos recursos dinâmicos

Um bloco with tradicional funciona muito bem quando todos os recursos são conhecidos no momento em que o código é escrito. Por exemplo:

with open("entrada.txt") as entrada, open("saida.txt", "w") as saida:
    saida.write(entrada.read())

Mas imagine que uma lista de arquivos venha de uma configuração, de um diretório ou de uma requisição. A quantidade pode variar. Montar vários blocos manualmente não é possível, e abrir arquivos em um loop exige cuidado para garantir que todos sejam fechados se uma abertura falhar no meio do processo.

Primeiro exemplo com ExitStack

from contextlib import ExitStack

caminhos = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
    arquivos = [
        stack.enter_context(open(caminho, encoding="utf-8"))
        for caminho in caminhos
    ]
    conteudos = [arquivo.read() for arquivo in arquivos]

print("Todos os arquivos foram fechados")

Cada chamada a enter_context() entra no context manager e registra automaticamente sua saída. Quando o bloco termina, os recursos são encerrados em ordem inversa. Essa ordem LIFO é importante porque frequentemente o último recurso adquirido depende dos anteriores.

Como a pilha de saída funciona

O ExitStack mantém uma pilha de funções de saída. Ao entrar em um context manager, ele registra o método __exit__(). Ao adicionar um callback, registra a função indicada. No encerramento, percorre a pilha do último item para o primeiro.

Essa abordagem imita o comportamento de blocos with aninhados, mas permite construir a estrutura durante a execução. O resultado é um fluxo claro: adquirir, registrar e trabalhar. A limpeza fica centralizada.

Abrir quantidade variável de arquivos

from contextlib import ExitStack
from pathlib import Path


def juntar_arquivos(origens, destino):
    with ExitStack() as stack:
        entradas = [
            stack.enter_context(Path(c).open(encoding="utf-8"))
            for c in origens
        ]
        saida = stack.enter_context(
            Path(destino).open("w", encoding="utf-8")
        )

        for entrada in entradas:
            saida.write(entrada.read())
            saida.write("\n")

Se o terceiro arquivo falhar ao abrir, os dois primeiros já registrados serão fechados. Isso evita vazamentos e torna o tratamento de erro muito mais seguro do que guardar handles em uma lista e tentar fechá-los manualmente depois.

Registrar callbacks com callback

Nem todo recurso implementa o protocolo de context manager. O método callback() permite registrar uma função comum para ser executada no encerramento.

from contextlib import ExitStack
from pathlib import Path

with ExitStack() as stack:
    temporario = Path("processamento.tmp")
    temporario.write_text("dados", encoding="utf-8")
    stack.callback(temporario.unlink, missing_ok=True)

    # processamento

# o arquivo temporário é removido

Callbacks recebem os argumentos informados no registro. Diferentemente de um método __exit__(), eles não recebem automaticamente informações sobre exceções e não podem suprimi-las.

Usar push para métodos de saída

O método push() aceita um context manager ou uma função compatível com a assinatura de __exit__(). É útil quando a entrada já aconteceu, mas você deseja transferir apenas a responsabilidade da saída.

from contextlib import ExitStack

recurso = criar_recurso()
recurso.__enter__()

with ExitStack() as stack:
    stack.push(recurso)
    usar(recurso)

Esse padrão exige cuidado. Em geral, prefira enter_context(), porque ele mantém entrada e registro juntos. Use push() quando a API realmente separar essas etapas.

Cancelar a limpeza com pop_all

Às vezes, você quer adquirir vários recursos e manter todos abertos somente se o conjunto inteiro for criado com sucesso. O método pop_all() transfere a pilha de callbacks para um novo ExitStack.

from contextlib import ExitStack


def abrir_todos(caminhos):
    stack = ExitStack()
    try:
        arquivos = [
            stack.enter_context(open(c, encoding="utf-8"))
            for c in caminhos
        ]
    except Exception:
        stack.close()
        raise

    pilha_final = stack.pop_all()
    return arquivos, pilha_final

arquivos, fechamento = abrir_todos(["a.txt", "b.txt"])
try:
    print([a.readline() for a in arquivos])
finally:
    fechamento.close()

Depois de pop_all(), o stack original não fecha mais os recursos transferidos. A responsabilidade passa explicitamente para o objeto retornado.

Tratamento de exceções durante a aquisição

Uma das maiores vantagens do ExitStack é garantir limpeza parcial. Considere uma sequência que abre arquivo, obtém lock e inicia uma transação. Se a transação falhar, tudo que já foi registrado é revertido ou encerrado.

from contextlib import ExitStack

with ExitStack() as stack:
    arquivo = stack.enter_context(open("dados.csv", encoding="utf-8"))
    lock = stack.enter_context(obter_lock("importacao"))
    transacao = stack.enter_context(banco.transacao())
    processar(arquivo, transacao)

Se obter_lock() falhar, o arquivo é fechado. Se a transação falhar ao entrar, lock e arquivo são liberados. Esse comportamento reduz estados intermediários perigosos.

Separar exceções de entrada e de uso

Em alguns casos, você precisa saber se o erro ocorreu ao entrar no context manager ou dentro do bloco de trabalho. É possível chamar enter_context() antes do bloco protegido principal.

from contextlib import ExitStack

stack = ExitStack()
try:
    recurso = stack.enter_context(conectar())
except OSError as erro:
    tratar_falha_de_conexao(erro)
else:
    with stack:
        executar_tarefa(recurso)

Esse formato evita capturar acidentalmente erros produzidos por executar_tarefa() como se fossem falhas de conexão.

Combinar context managers opcionais

ExitStack é adequado quando um recurso só deve ser adquirido em certas condições.

from contextlib import ExitStack, nullcontext

with ExitStack() as stack:
    arquivo = stack.enter_context(open("dados.txt", encoding="utf-8"))

    if usar_lock:
        stack.enter_context(lock_distribuido("dados"))

    if transacao_ativa:
        stack.enter_context(banco.transacao())

    processar(arquivo)

Isso evita árvores complexas de if contendo diferentes combinações de blocos with.

Callbacks para rollback

Você pode registrar operações compensatórias à medida que uma tarefa progride.

from contextlib import ExitStack

with ExitStack() as stack:
    usuario = criar_usuario()
    stack.callback(remover_usuario, usuario.id)

    perfil = criar_perfil(usuario.id)
    stack.callback(remover_perfil, perfil.id)

    confirmar_operacao()
    stack.pop_all()

Se qualquer etapa falhar, os callbacks executam rollback em ordem inversa. Se tudo der certo, pop_all() descarta a limpeza pendente. Esse padrão é útil, mas não substitui transações reais quando o sistema oferece atomicidade.

Não esconda erros de limpeza

Uma função de encerramento também pode falhar. Evite callbacks que ignorem exceções importantes. Registre logs suficientes e decida se a falha deve substituir, encadear ou apenas complementar a exceção principal.

Quando múltiplas saídas levantam erros, a semântica segue o encadeamento de exceções dos context managers. Teste cenários de falha, principalmente em integrações com rede, banco e sistema de arquivos.

AsyncExitStack para código assíncrono

O módulo contextlib também oferece AsyncExitStack para context managers assíncronos.

from contextlib import AsyncExitStack

async with AsyncExitStack() as stack:
    sessao = await stack.enter_async_context(criar_sessao())
    resposta = await stack.enter_async_context(sessao.get(url))
    dados = await resposta.json()

Ele pode registrar saídas síncronas e assíncronas, mas os métodos corretos devem ser usados: enter_async_context(), push_async_exit() e push_async_callback().

Testar o fechamento dos recursos

Crie context managers falsos que registrem eventos.

from contextlib import contextmanager, ExitStack


eventos = []

@contextmanager
def recurso(nome):
    eventos.append(f"entra:{nome}")
    try:
        yield nome
    finally:
        eventos.append(f"sai:{nome}")

with ExitStack() as stack:
    stack.enter_context(recurso("A"))
    stack.enter_context(recurso("B"))

assert eventos == ["entra:A", "entra:B", "sai:B", "sai:A"]

Teste também falha na segunda entrada, exceção durante o uso, callback com argumentos e transferência por pop_all().

Erros frequentes

  • Usar push() quando enter_context() seria mais seguro.
  • Esquecer de fechar um ExitStack criado fora de with.
  • Chamar pop_all() sem assumir a responsabilidade pelo novo stack.
  • Registrar callbacks que capturam objetos mutáveis de forma inesperada.
  • Confundir callbacks comuns com funções capazes de suprimir exceções.
  • Usar rollback manual onde uma transação do banco seria mais robusta.
  • Não testar a ordem inversa de encerramento.

Boas práticas

  • Registre cada recurso imediatamente após adquiri-lo.
  • Prefira with ExitStack() para garantir o fechamento.
  • Mantenha callbacks pequenos, determinísticos e idempotentes.
  • Documente quem assume a pilha após pop_all().
  • Separe erros de entrada dos erros de processamento quando necessário.
  • Use AsyncExitStack em fluxos assíncronos.
  • Teste falhas em cada etapa da aquisição.

Quando usar ExitStack

Use ExitStack quando a quantidade de recursos for dinâmica, quando recursos forem opcionais, quando diferentes tipos de limpeza precisarem ser coordenados ou quando uma aquisição em várias etapas exigir rollback previsível. Para dois recursos fixos e simples, um bloco with comum continua mais legível.

Conclusão

O ExitStack no Python transforma a gestão dinâmica de recursos em uma pilha explícita e segura. Ele mantém a limpeza próxima da aquisição, fecha recursos em ordem inversa e permite combinar context managers, callbacks e transferências de responsabilidade.

Use a ferramenta com disciplina: prefira entradas registradas imediatamente, mantenha callbacks claros e teste todas as rotas de erro. Consulte a documentação oficial do ExitStack e a referência do protocolo de context managers para aprofundar o comportamento.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código assíncrono representando asyncio.eager_task_factory no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.eager_task_factory: reduza overhead de tarefas

    Aprenda asyncio.eager_task_factory no Python para reduzir overhead, entender mudanças de ordem e otimizar corrotinas curtas com segurança.

    Ler mais

    Tempo de leitura: 4 minutos
    14/09/2026
    Desenvolvedor trabalhando com timestamps UTC e calendar.timegm no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    calendar.timegm: converta UTC para timestamp Unix

    Aprenda calendar.timegm no Python para converter datas UTC em timestamps Unix com segurança, testes e integração com datetime.

    Ler mais

    Tempo de leitura: 6 minutos
    14/09/2026
    Programador analisando código para identificar tipos MIME de arquivos no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    mimetypes.guess_file_type: detecte tipos MIME

    Aprenda mimetypes.guess_file_type no Python para detectar tipos MIME em caminhos, URLs, uploads e respostas HTTP com fallbacks seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026
    Componentes de servidor representando interpretadores Python executando em paralelo
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    InterpreterPoolExecutor: paralelismo real no Python

    Aprenda InterpreterPoolExecutor no Python para executar tarefas CPU-bound em interpretadores isolados com paralelismo real.

    Ler mais

    Tempo de leitura: 6 minutos
    13/09/2026
    Microprocessador representando CPUs disponíveis para um processo Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    os.process_cpu_count: conte CPUs disponíveis

    Aprenda os.process_cpu_count no Python para dimensionar workers conforme as CPUs realmente disponíveis ao processo.

    Ler mais

    Tempo de leitura: 4 minutos
    12/09/2026
    Laptop com código digital representando dados BLOB no SQLite
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    sqlite3.Blob: leia BLOBs sem carregar tudo na memória

    Aprenda sqlite3.Blob no Python para ler e gravar BLOBs em partes, reduzir memória e trabalhar com dados binários no SQLite.

    Ler mais

    Tempo de leitura: 5 minutos
    12/09/2026