contextlib.ExitStack é uma ferramenta da biblioteca padrão do Python criada para gerenciar recursos cujo número ou tipo só é conhecido em tempo de execução. Ela funciona como uma pilha de context managers e callbacks de limpeza. Em vez de escrever vários blocos with aninhados, você registra recursos em uma pilha e garante que todos sejam liberados na ordem inversa, mesmo quando ocorre uma exceção.
O problema que ExitStack resolve
O comando with é excelente quando você sabe antecipadamente quais recursos serão usados. Por exemplo, abrir dois arquivos é simples com with open(...), open(...). O problema aparece quando a quantidade de arquivos depende de uma lista, quando alguns recursos são opcionais ou quando parte da limpeza não vem de um context manager tradicional.
from contextlib import ExitStack
caminhos = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
arquivos = [stack.enter_context(open(c, encoding="utf-8")) for c in caminhos]
conteudos = [arquivo.read() for arquivo in arquivos]
Ao sair do bloco, todos os arquivos são fechados automaticamente. Isso também acontece se a leitura do segundo arquivo falhar. Para revisar o funcionamento básico de contextos, veja como usar with no Python e tratamento de erros com try except.
Como a pilha de saída funciona
Cada recurso registrado adiciona uma ação de saída. Quando o bloco termina, as ações são executadas em ordem LIFO: o último recurso registrado é o primeiro a ser liberado. Esse comportamento reproduz o encerramento natural de blocos with aninhados.
O método mais usado é enter_context. Ele recebe um context manager, chama seu método __enter__, devolve o valor produzido e registra o __exit__ para execução posterior.
with ExitStack() as stack:
arquivo = stack.enter_context(open("dados.txt", encoding="utf-8"))
conexao = stack.enter_context(criar_conexao())
# ambos serão encerrados corretamente
Registrando callbacks de limpeza
Nem todo recurso implementa o protocolo de context manager. Para esses casos, use callback. O método recebe uma função e seus argumentos, executando-a quando a pilha for fechada.
from pathlib import Path
from contextlib import ExitStack
pasta = Path("temporario")
pasta.mkdir(exist_ok=True)
with ExitStack() as stack:
stack.callback(lambda: pasta.rmdir())
arquivo = pasta / "saida.txt"
arquivo.write_text("resultado", encoding="utf-8")
stack.callback(arquivo.unlink)
Como a remoção do arquivo foi registrada depois da remoção da pasta, ela será executada primeiro. Isso evita tentar apagar uma pasta ainda ocupada.
O método push
O método push registra diretamente uma função compatível com __exit__ ou um objeto que implemente esse método. Ele é útil quando você já iniciou parte de uma operação e deseja transferir apenas a responsabilidade de saída para a pilha.
recurso = MeuRecurso()
recurso.iniciar()
with ExitStack() as stack:
stack.push(recurso)
recurso.executar()
Diferentemente de enter_context, push não chama __enter__. Essa diferença é importante para evitar inicialização duplicada.
Recursos opcionais
Uma vantagem prática é adicionar recursos condicionalmente sem duplicar blocos. Um programa pode abrir um arquivo de log apenas quando o usuário habilita depuração.
from contextlib import ExitStack, nullcontext
with ExitStack() as stack:
log = stack.enter_context(
open("debug.log", "a", encoding="utf-8") if modo_debug else nullcontext(None)
)
if log:
log.write("execução iniciada\n")
Outra opção é simplesmente chamar enter_context dentro de um if. Para caminhos e arquivos, consulte também pathlib no Python.
Validação antes de confirmar
ExitStack ajuda a implementar operações em duas fases. Primeiro você adquire recursos e valida condições. Depois decide se mantém ou desfaz as ações. O método pop_all transfere os callbacks para uma nova pilha sem executá-los.
stack = ExitStack()
try:
arquivos = [stack.enter_context(open(c, encoding="utf-8")) for c in caminhos]
validar(arquivos)
nova_pilha = stack.pop_all()
finally:
stack.close()
Esse padrão é útil para construtores complexos, importações de dados e fluxos em que uma falha parcial deve desfazer tudo.
Exemplo com vários arquivos
Imagine um utilitário que combina diversos arquivos CSV. A lista pode variar conforme a entrada do usuário. Com ExitStack, o código permanece curto e seguro.
import csv
from contextlib import ExitStack
def combinar(caminhos):
with ExitStack() as stack:
arquivos = [stack.enter_context(open(c, newline="", encoding="utf-8")) for c in caminhos]
leitores = [csv.DictReader(a) for a in arquivos]
return [linha for leitor in leitores for linha in leitor]
Para aprofundar o formato, veja arquivos CSV no Python.
Tratamento de exceções
Os callbacks registrados por callback não recebem informações da exceção e não podem suprimi-la. Já funções registradas com push seguem a assinatura de __exit__ e podem observar ou suprimir exceções. Em aplicações comuns, é melhor evitar supressão silenciosa, pois ela pode esconder erros importantes.
Se uma ação de limpeza falhar, as demais ainda podem ser executadas, mas a exceção final pode mudar conforme a sequência de callbacks. Por isso, cada etapa de limpeza deve ser pequena, previsível e idempotente quando possível.
ExitStack em testes
Em testes automatizados, a classe facilita registrar patches, arquivos temporários e objetos simulados de forma dinâmica. Isso reduz a necessidade de vários decoradores e deixa claro quais recursos pertencem ao teste.
from contextlib import ExitStack
from unittest.mock import patch
with ExitStack() as stack:
mock_a = stack.enter_context(patch("modulo.funcao_a"))
mock_b = stack.enter_context(patch("modulo.funcao_b"))
executar_fluxo()
Para boas práticas de testes, consulte testes unitários no Python.
AsyncExitStack
Para recursos assíncronos existe contextlib.AsyncExitStack. Ela oferece métodos como enter_async_context, push_async_exit e push_async_callback. O conceito é o mesmo, mas a saída é aguardada com await.
from contextlib import AsyncExitStack
async with AsyncExitStack() as stack:
sessao = await stack.enter_async_context(criar_sessao())
canal = await stack.enter_async_context(abrir_canal())
Esse recurso combina bem com aplicações que usam rede, bancos de dados ou filas assíncronas. Veja também asyncio no Python.
Boas práticas
Use ExitStack quando a quantidade de recursos for dinâmica, quando houver recursos opcionais ou quando você precisar misturar context managers e callbacks. Para casos simples e fixos, um bloco with tradicional continua mais legível.
Registre a limpeza imediatamente após adquirir cada recurso. Isso reduz a janela em que uma exceção poderia deixar algo aberto. Evite callbacks grandes. Prefira funções pequenas e específicas. Documente quando push é usado sem __enter__, pois esse detalhe pode confundir outros desenvolvedores.
Não use a classe como substituto genérico para tratamento de erros. Ela organiza a liberação de recursos, mas não elimina a necessidade de validação, logs e mensagens claras.
Conclusão
contextlib.ExitStack oferece uma maneira elegante de gerenciar conjuntos dinâmicos de recursos. Ela mantém o comportamento seguro do with, permite registrar callbacks comuns e garante fechamento em ordem inversa. Em scripts de automação, processamento de arquivos, testes e serviços que combinam vários recursos, a classe reduz aninhamento e torna a intenção do código mais explícita.
Referências oficiais: documentação do ExitStack e PEP 343 sobre o comando with.







