contextlib en Python: @contextmanager, suppress, nullcontext y gestores propios

Un gestor de contexto es cualquier objeto que implementa __enter__ y __exit__, el mecanismo que está detrás de with open(...) as f. Escribir uno desde cero requiere una clase completa. El módulo contextlib proporciona herramientas para crear gestores de contexto de forma mucho más sencilla.

@contextmanager: gestor de contexto con un generador

El decorador @contextmanager convierte un generador en un gestor de contexto. El código antes del yield actúa como __enter__; el código después, como __exit__. El valor del yield es lo que recibe la cláusula as.

from contextlib import contextmanager
import time

@contextmanager
def timer(etiqueta: str):
    inicio = time.perf_counter()
    try:
        yield  # aquí se ejecuta el bloque with
    finally:
        fin = time.perf_counter()
        print(f"[{etiqueta}] {fin - inicio:.3f}s")

with timer("ordenar lista"):
    datos = list(range(100_000, 0, -1))
    datos.sort()
# [ordenar lista] 0.012s
from contextlib import contextmanager
import sqlite3

@contextmanager
def transaccion(ruta_db: str):
    """Gestiona una transacción: commit si todo va bien, rollback si hay excepción."""
    conn = sqlite3.connect(ruta_db)
    try:
        yield conn
        conn.commit()
        print("Commit realizado")
    except Exception as e:
        conn.rollback()
        print(f"Rollback por: {e}")
        raise
    finally:
        conn.close()

with transaccion("mi_base.db") as conn:
    conn.execute("INSERT INTO usuarios (nombre) VALUES (?)", ("Ana",))

contextlib.suppress — ignorar excepciones específicas

suppress(*excepciones) suprime las excepciones listadas si se lanzan dentro del bloque. Reemplaza bloques try/except: pass.

from contextlib import suppress
import os

# Sin suppress:
try:
    os.remove("fichero_que_puede_no_existir.txt")
except FileNotFoundError:
    pass

# Con suppress:
with suppress(FileNotFoundError):
    os.remove("fichero_que_puede_no_existir.txt")

# También con múltiples tipos de excepción:
with suppress(FileNotFoundError, PermissionError):
    os.remove("/ruta/protegida.txt")

contextlib.nullcontext — gestor de contexto vacío

nullcontext es útil cuando necesitas un gestor de contexto de forma condicional pero no quieres añadir lógica extra.

from contextlib import nullcontext
import threading

def procesar(datos: list, usar_lock: bool = False):
    lock = threading.Lock() if usar_lock else nullcontext()
    with lock:
        # El código funciona igual con o sin lock
        resultado = sum(datos)
    return resultado

print(procesar([1, 2, 3]))           # sin lock
print(procesar([1, 2, 3], usar_lock=True))  # con lock
from contextlib import nullcontext

def abrir_fichero(path: str | None):
    # Si no hay path, usa nullcontext; si hay, abre el fichero
    ctx = open(path) if path else nullcontext()
    with ctx as f:
        if f:
            contenido = f.read()
        else:
            contenido = "sin fichero"
    return contenido

ExitStack — gestionar un número variable de contextos

ExitStack te permite apilar dinámicamente cualquier número de gestores de contexto, algo que no puedes hacer con with estático.

from contextlib import ExitStack

rutas = ["archivo1.txt", "archivo2.txt", "archivo3.txt"]

with ExitStack() as stack:
    ficheros = [stack.enter_context(open(r)) for r in rutas]
    for f in ficheros:
        print(f.readline().strip())
# Todos los ficheros se cierran al salir del bloque
from contextlib import ExitStack, contextmanager

@contextmanager
def recurso(nombre: str):
    print(f"Abriendo {nombre}")
    try:
        yield nombre
    finally:
        print(f"Cerrando {nombre}")

# Gestores de contexto definidos dinámicamente en tiempo de ejecución
nombres = ["db", "cache", "log"]
with ExitStack() as stack:
    recursos = [stack.enter_context(recurso(n)) for n in nombres]
    print(f"Trabajando con: {recursos}")
# Abriendo db / Abriendo cache / Abriendo log
# Trabajando con: ['db', 'cache', 'log']
# Cerrando log / Cerrando cache / Cerrando db  (orden inverso)

@asynccontextmanager para código async

from contextlib import asynccontextmanager
import asyncio

@asynccontextmanager
async def conexion_async(host: str):
    print(f"Conectando a {host}...")
    await asyncio.sleep(0.1)  # simula conexión
    try:
        yield {"host": host, "activa": True}
    finally:
        print(f"Cerrando conexión a {host}")
        await asyncio.sleep(0.05)

async def main():
    async with conexion_async("db.ejemplo.com") as conn:
        print(f"Usando conexión: {conn}")

asyncio.run(main())

La regla para elegir herramienta: @contextmanager para la mayoría de los casos; suppress en lugar de try/except: pass; nullcontext para contextos opcionales; ExitStack cuando el número de recursos no se conoce hasta tiempo de ejecución. Evita gestores de contexto que silencien excepciones inesperadas: suppress solo debe usarse con los tipos exactos de error que esperas.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP