ExitStack en Python: gestiona recursos

Publicado el: 11/08/2026
Tempo de leitura: 6 minutos
Red de servidores que representa la gestión de recursos con ExitStack en Python

Gestionar varios archivos, conexiones, bloqueos, transacciones y acciones de limpieza puede producir bloques with profundamente anidados. ExitStack en Python, disponible en el módulo contextlib, permite registrar context managers y callbacks de forma dinámica. Es especialmente útil cuando la cantidad o el tipo de recursos solo se conoce durante la ejecución.

En esta guía aprenderás cómo funciona ExitStack, por qué libera recursos en orden inverso, cómo abrir una cantidad variable de archivos, registrar callbacks comunes, transferir la responsabilidad de cierre, manejar fallos parciales y probar cada ruta de salida. El tema complementa nuestros artículos sobre asyncio, tempfile, zipfile, filecmp y zoneinfo.

El problema de los recursos dinámicos

Una sentencia with normal funciona perfectamente cuando todos los recursos se conocen al escribir el código:

with open("entrada.txt") as origen, open("salida.txt", "w") as destino:
    destino.write(origen.read())

Sin embargo, una aplicación puede recibir una lista de rutas desde una configuración, descubrir archivos en una carpeta o decidir en tiempo de ejecución si necesita un lock, una transacción o una sesión de red. Abrir recursos dentro de un bucle exige garantizar que todos los ya adquiridos se cierren si una operación posterior falla.

Primer ejemplo con ExitStack

from contextlib import ExitStack

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

with ExitStack() as stack:
    archivos = [
        stack.enter_context(open(ruta, encoding="utf-8"))
        for ruta in rutas
    ]
    contenidos = [archivo.read() for archivo in archivos]

print("Todos los archivos están cerrados")

enter_context() entra en cada context manager y registra inmediatamente su método de salida. Cuando el stack se cierra, las salidas se ejecutan desde la más reciente hasta la más antigua. Este orden LIFO reproduce el comportamiento de varios bloques with anidados.

Cómo funciona la pila de salida

ExitStack guarda funciones de salida en una pila interna. Al entrar en un context manager añade su método __exit__(). Al registrar un callback añade la función indicada. Al finalizar, ejecuta todo en orden inverso.

La ventaja es que la estructura se construye durante la ejecución sin perder previsibilidad. La adquisición y la limpieza permanecen claramente relacionadas.

Abrir una cantidad variable de archivos

from contextlib import ExitStack
from pathlib import Path


def unir_archivos(origenes, destino):
    with ExitStack() as stack:
        entradas = [
            stack.enter_context(Path(ruta).open(encoding="utf-8"))
            for ruta in origenes
        ]
        salida = stack.enter_context(
            Path(destino).open("w", encoding="utf-8")
        )

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

Si el tercer archivo no puede abrirse, los dos primeros ya están registrados y serán cerrados. Esto es más seguro que guardar handles en una lista y tratar de liberarlos manualmente después de una excepción.

Registrar limpieza con callback

No todos los recursos implementan el protocolo de context manager. El método callback() registra una función común con sus argumentos.

from contextlib import ExitStack
from pathlib import Path

with ExitStack() as stack:
    temporal = Path("proceso.tmp")
    temporal.write_text("datos", encoding="utf-8")
    stack.callback(temporal.unlink, missing_ok=True)

    procesar(temporal)

# el archivo temporal se elimina

Un callback normal no recibe automáticamente información sobre la excepción y no puede suprimirla. Debe realizar una limpieza pequeña, predecible e idealmente idempotente.

Usar push para salidas existentes

push() acepta un context manager o una función compatible con la firma de __exit__(). Resulta útil cuando un recurso ya fue abierto y solo necesitas transferir su salida.

from contextlib import ExitStack

recurso = crear_recurso()
recurso.__enter__()

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

Siempre que sea posible, prefiere enter_context(), porque mantiene adquisición y registro en una única operación. Usa push() solo cuando la API separe deliberadamente ambas fases.

Transferir responsabilidad con pop_all

Una función puede necesitar abrir un grupo completo y devolverlo todavía activo únicamente si todas las adquisiciones tienen éxito. pop_all() mueve las salidas pendientes a un nuevo ExitStack.

from contextlib import ExitStack


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

    propietario = stack.pop_all()
    return archivos, propietario

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

Después de pop_all(), el stack original deja de ser responsable. El objeto retornado debe cerrarse explícitamente o usarse dentro de otro bloque with.

Seguridad ante adquisición parcial

Una ventaja central de ExitStack es limpiar correctamente después de un éxito parcial. Considera un proceso que abre un archivo, adquiere un lock y comienza una transacción:

from contextlib import ExitStack

with ExitStack() as stack:
    archivo = stack.enter_context(open("datos.csv", encoding="utf-8"))
    lock = stack.enter_context(obtener_lock("importacion"))
    transaccion = stack.enter_context(base.transaccion())
    importar(archivo, transaccion)

Si el lock falla, el archivo se cierra. Si la transacción falla al entrar, se liberan el lock y el archivo. El código evita estados intermedios ocultos.

Distinguir errores de entrada y uso

A veces es necesario saber si el error ocurrió al adquirir el recurso o dentro del trabajo principal.

from contextlib import ExitStack

stack = ExitStack()
try:
    conexion = stack.enter_context(conectar())
except OSError as error:
    tratar_fallo_conexion(error)
else:
    with stack:
        ejecutar_tarea(conexion)

Este formato evita interpretar un OSError producido por ejecutar_tarea() como si fuera un error de conexión.

Context managers opcionales

ExitStack simplifica recursos condicionales.

from contextlib import ExitStack

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

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

    if usar_transaccion:
        stack.enter_context(base.transaccion())

    procesar(archivo)

Así se evita una gran estructura de condiciones con todas las combinaciones posibles de bloques with.

Acciones compensatorias y rollback

Los callbacks pueden representar acciones compensatorias para operaciones de varias etapas.

from contextlib import ExitStack

with ExitStack() as stack:
    usuario = crear_usuario()
    stack.callback(eliminar_usuario, usuario.id)

    perfil = crear_perfil(usuario.id)
    stack.callback(eliminar_perfil, perfil.id)

    confirmar_operacion()
    stack.pop_all()

Si una etapa falla, la limpieza se ejecuta en orden inverso. Si todo termina bien, pop_all() elimina el plan de rollback. Aun así, una transacción real es preferible cuando el sistema ofrece atomicidad.

Los errores de limpieza también importan

Las funciones de salida pueden fallar. No ignores silenciosamente errores al cerrar conexiones, vaciar buffers, revertir transacciones o borrar archivos temporales. Decide cómo deben registrarse y encadenarse con la excepción original.

Prueba escenarios donde el cuerpo falla y una o varias salidas también fallan. Esos casos muestran si la aplicación conserva la información de diagnóstico más útil.

AsyncExitStack

Los programas asíncronos disponen de AsyncExitStack.

from contextlib import AsyncExitStack

async with AsyncExitStack() as stack:
    sesion = await stack.enter_async_context(crear_sesion())
    respuesta = await stack.enter_async_context(sesion.get(url))
    datos = await respuesta.json()

AsyncExitStack admite limpieza síncrona y asíncrona, pero debes usar los métodos adecuados: enter_async_context(), push_async_exit() y push_async_callback().

Probar el orden de salida

from contextlib import contextmanager, ExitStack

eventos = []

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

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

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

Prueba también un fallo durante la segunda entrada, una excepción en el cuerpo, callbacks con argumentos, pop_all() y errores de cierre.

Errores frecuentes

  • Usar push() cuando enter_context() sería más seguro.
  • Crear un ExitStack fuera de with y olvidar cerrarlo.
  • Llamar pop_all() sin transferir claramente la propiedad.
  • Registrar callbacks que capturan valores mutables cambiantes.
  • Suponer que un callback normal puede suprimir excepciones.
  • Implementar rollback manual cuando una transacción sería más fiable.
  • No probar el orden inverso de limpieza.

Buenas prácticas

  • Registra cada recurso inmediatamente después de adquirirlo.
  • Prefiere with ExitStack() para cierre automático.
  • Mantén callbacks pequeños, deterministas e idempotentes.
  • Documenta la propiedad después de pop_all().
  • Separa errores de adquisición y procesamiento cuando sea necesario.
  • Usa AsyncExitStack en flujos asíncronos.
  • Prueba fallos en cada etapa.

Cuándo usar ExitStack

Usa ExitStack cuando la cantidad de recursos sea dinámica, algunos recursos sean opcionales, distintos mecanismos de limpieza deban coordinarse o una adquisición de varias etapas necesite rollback predecible. Para dos recursos fijos y simples, una sentencia with común suele ser más clara.

Conclusión

ExitStack en Python convierte la gestión dinámica de recursos en una pila explícita y fiable. Cierra en orden inverso, combina context managers con callbacks y permite transferir la responsabilidad cuando es necesario.

Registra la limpieza en cuanto adquieras cada recurso, mantén callbacks comprensibles y prueba todas las rutas de error. Consulta la documentación oficial de ExitStack y la referencia del protocolo de context managers para ampliar los detalles.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Alfabeto tridimensional que representa normalización Unicode con unicodedata en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    unicodedata en Python: normaliza Unicode

    Aprende unicodedata en Python para normalizar Unicode, consultar nombres, categorías, números, marcas combinantes y ancho de visualización.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Carpeta con candado que representa tipos y permisos con stat en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    stat en Python: tipos y permisos

    Aprende stat en Python para interpretar tipos de archivo, permisos, enlaces, timestamps, atributos de Windows y flags de Unix con

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Portátil con código que representa documentación automática con pydoc en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pydoc en Python: documentación automática

    Aprende pydoc en Python para generar ayuda en terminal, HTML, búsqueda y un servidor local de documentación de forma segura.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026
    Teclado internacional que representa números, moneda y fechas con locale en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    locale en Python: números, moneda y fechas

    Aprende locale en Python para formatear e interpretar números, moneda, fechas, encodings y orden cultural sin errores de concurrencia.

    Ler mais

    Tempo de leitura: 6 minutos
    09/08/2026
    Monitor y red que representan información del sistema con platform en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    platform en Python: información del sistema

    Aprende platform en Python para identificar sistema operativo, arquitectura, distribución, versión de Python y entorno de ejecución.

    Ler mais

    Tempo de leitura: 5 minutos
    09/08/2026
    Código y compilador que representan rutas y variables de build con sysconfig en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sysconfig en Python: rutas y build

    Aprende sysconfig en Python para descubrir rutas de instalación, variables de build, headers, virtualenvs y plataformas de forma segura.

    Ler mais

    Tempo de leitura: 6 minutos
    09/08/2026