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 é removidoCallbacks 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()quandoenter_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
AsyncExitStackem 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.







