contextlib.ExitStack es una utilidad de la biblioteca estándar de Python para gestionar una colección dinámica de recursos. Funciona como una pila programable de context managers y callbacks de limpieza. En lugar de anidar muchos bloques with, registras cada recurso cuando lo adquieres y dejas que la pila libere todo en orden inverso, incluso cuando una excepción interrumpe la operación.
Qué problema resuelve ExitStack
Un bloque with normal es ideal cuando conoces los recursos de antemano. El problema aparece cuando la cantidad de archivos depende de una lista, una conexión es opcional o una operación de limpieza no implementa el protocolo de context manager.
from contextlib import ExitStack
rutas = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
archivos = [stack.enter_context(open(r, encoding="utf-8")) for r in rutas]
textos = [archivo.read() for archivo in archivos]
Todos los archivos se cierran automáticamente. Si falla la lectura de uno, los anteriores también se liberan correctamente. Repasa los fundamentos en la sentencia with en Python y el tratamiento de excepciones.
Cómo funciona la pila de salida
Cada recurso registrado añade una acción de salida. Cuando termina el bloque, las acciones se ejecutan en orden LIFO: la última registrada es la primera que se libera. Es el mismo comportamiento que tendrían varios context managers anidados.
El método más habitual es enter_context. Este llama a __enter__, devuelve el valor producido y guarda __exit__ para ejecutarlo después.
with ExitStack() as stack:
archivo = stack.enter_context(open("datos.txt", encoding="utf-8"))
conexion = stack.enter_context(crear_conexion())
Registrar callbacks de limpieza
No todos los recursos implementan un context manager. El método callback registra una función normal y sus argumentos para ejecutarla cuando se cierre la pila.
from pathlib import Path
from contextlib import ExitStack
carpeta = Path("temporal")
carpeta.mkdir(exist_ok=True)
with ExitStack() as stack:
stack.callback(carpeta.rmdir)
salida = carpeta / "resultado.txt"
salida.write_text("listo", encoding="utf-8")
stack.callback(salida.unlink)
La eliminación del archivo fue registrada al final y se ejecuta primero. Después se elimina la carpeta vacía. Para trabajar con rutas, consulta pathlib en Python.
El método push
push registra directamente un método __exit__ o una función compatible. A diferencia de enter_context, no llama a __enter__. Es útil cuando el recurso ya fue iniciado y solo quieres transferir la responsabilidad de salida.
recurso = Recurso()
recurso.iniciar()
with ExitStack() as stack:
stack.push(recurso)
recurso.ejecutar()
Esta diferencia evita inicializaciones duplicadas y debe quedar documentada para que el código sea fácil de mantener.
Recursos opcionales
ExitStack permite adquirir recursos de forma condicional sin duplicar todo el bloque. Puedes abrir un log, iniciar una transacción o adquirir un bloqueo únicamente cuando sea necesario.
with ExitStack() as stack:
if depuracion:
log = stack.enter_context(open("debug.log", "a", encoding="utf-8"))
log.write("inicio\n")
ejecutar_tarea()
También puedes combinar la pila con nullcontext cuando necesitas una expresión uniforme.
Procesar varios archivos
Un caso frecuente es combinar una cantidad variable de archivos CSV. ExitStack mantiene abiertos todos los streams durante el procesamiento y los cierra de forma fiable al terminar.
import csv
from contextlib import ExitStack
def combinar(rutas):
with ExitStack() as stack:
archivos = [stack.enter_context(open(r, newline="", encoding="utf-8")) for r in rutas]
lectores = [csv.DictReader(a) for a in archivos]
return [fila for lector in lectores for fila in lector]
Amplía el tema en archivos CSV en Python.
Separar adquisición y confirmación
El método pop_all transfiere los callbacks registrados a una nueva pila sin ejecutarlos. Esto permite crear operaciones en dos fases: primero adquirir y validar; después decidir quién conserva la responsabilidad de limpieza.
stack = ExitStack()
try:
recursos = [stack.enter_context(open(r, encoding="utf-8")) for r in rutas]
validar(recursos)
propietario = stack.pop_all()
finally:
stack.close()
Este patrón es útil en constructores complejos, importaciones por lotes y procesos que deben deshacer todo cuando una etapa intermedia falla.
Comportamiento ante excepciones
Los callbacks registrados con callback no reciben datos de la excepción y no pueden suprimirla. Las funciones de salida registradas con push reciben el tipo, el valor y el traceback, y pueden suprimir la excepción devolviendo un valor verdadero.
La supresión silenciosa debe usarse con cuidado porque puede ocultar errores. Las funciones de limpieza deben ser pequeñas, previsibles e idempotentes cuando sea posible. Evita agrupar tareas no relacionadas en un único callback.
ExitStack en pruebas
Las pruebas automatizadas suelen necesitar patches, archivos temporales y mocks cuyo número cambia según el caso. ExitStack mantiene estos recursos dentro de un único alcance visible.
from contextlib import ExitStack
from unittest.mock import patch
with ExitStack() as stack:
mock_a = stack.enter_context(patch("modulo.funcion_a"))
mock_b = stack.enter_context(patch("modulo.funcion_b"))
ejecutar_flujo()
Consulta también pruebas unitarias en Python.
AsyncExitStack
Para aplicaciones asíncronas existe contextlib.AsyncExitStack. Incluye enter_async_context, push_async_exit y push_async_callback. Las operaciones de salida se esperan con await.
from contextlib import AsyncExitStack
async with AsyncExitStack() as stack:
sesion = await stack.enter_async_context(crear_sesion())
canal = await stack.enter_async_context(abrir_canal())
Resulta útil con clientes de red, bases de datos, colas y servicios asíncronos. Consulta asyncio en Python.
Buenas prácticas
Usa ExitStack cuando la cantidad de recursos sea dinámica, existan recursos opcionales o necesites combinar context managers con callbacks normales. Para dos o tres recursos fijos, un with tradicional suele ser más claro.
Registra la limpieza inmediatamente después de adquirir cada recurso. Mantén los callbacks pequeños. Documenta el uso de push, ya que no ejecuta __enter__. No uses ExitStack como sustituto de validación, logs, reintentos o mensajes de error. Su responsabilidad principal es gestionar el ciclo de vida.
Conclusión
contextlib.ExitStack convierte la liberación de recursos en un flujo flexible y explícito. Conserva la seguridad del comando with, pero admite decisiones en tiempo de ejecución, callbacks comunes, recursos opcionales y transferencia de propiedad. Es especialmente útil en scripts de automatización, procesamiento de archivos, pruebas y servicios que coordinan varios recursos externos.
Referencias oficiales: documentación de ExitStack y PEP 343.







