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

    Pasta com cadeado representando tipos e permissões com stat no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    stat no Python: tipos e permissões

    Aprenda stat no Python para interpretar tipos de arquivo, permissões, links, timestamps, atributos do Windows e flags de Unix com

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Notebook com código representando documentação automática com pydoc no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    pydoc no Python: documentação automática

    Aprenda pydoc no Python para gerar ajuda no terminal, HTML e servidor local a partir de docstrings, com segurança ao

    Ler mais

    Tempo de leitura: 7 minutos
    10/08/2026
    Teclado internacional representando números, moedas e datas com locale no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    locale no Python: números, moedas e datas

    Aprenda locale no Python para formatar e interpretar números, moedas, datas e ordenação cultural sem erros de concorrência.

    Ler mais

    Tempo de leitura: 8 minutos
    09/08/2026
    Monitor e rede representando informações de sistema com platform no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    platform no Python: informações do sistema

    Aprenda platform no Python para identificar sistema operacional, arquitetura, distribuição, versão do Python e ambiente com segurança.

    Ler mais

    Tempo de leitura: 7 minutos
    09/08/2026
    Código e compilador representando caminhos e variáveis de build com sysconfig no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    sysconfig no Python: caminhos e build

    Aprenda sysconfig no Python para descobrir caminhos de instalação, variáveis de build, headers, virtualenvs e plataformas com segurança.

    Ler mais

    Tempo de leitura: 7 minutos
    09/08/2026
    Disco rígido representando arquivos mapeados em memória com mmap no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    mmap no Python: arquivos em memória

    Aprenda mmap no Python para mapear arquivos em memória, pesquisar bytes, compartilhar dados e escolher leitura, escrita ou copy-on-write.

    Ler mais

    Tempo de leitura: 7 minutos
    09/08/2026