▷ Context managers en Python — with y cómo crear los tuyos 2026

Context managers en Python — with y cómo crear los tuyos

Cuando llevas dos semanas con Python, escribes with open("...") as f: cada día sin pensar. Ese with es la sintaxis de un context manager. Y es un patrón tan importante que casi todo lo serio en Python lo usa: ficheros, conexiones a BBDD, locks, transacciones, mocks en tests, sesiones HTTP.

La idea de fondo es muy simple: garantizar que algo se cierra/limpia/restaura, pase lo que pase. Aunque haya excepciones. Aunque vuelvas con return. Aunque el sistema arda. Es la versión limpia y cool de try/finally.

En esta entrada te enseño cómo funciona with por dentro, cómo crear tus propios context managers (con clase y con decorador), y cuándo te ahorran un montón de líneas de boilerplate frágil.

La idea en 30 segundos

Mira el patrón que probablemente has escrito mil veces:

f = open("datos.txt", "r")
try:
    contenido = f.read()
finally:
    f.close()

El finally garantiza que el fichero se cierra pase lo que pase. Si read() lanza una excepción, igual se cierra. Si haces return dentro del try, igual se cierra.

Eso, condensado en una línea elegante con with:

with open("datos.txt", "r") as f:
    contenido = f.read()
# fichero garantizado cerrado al salir del bloque

Eso es un context manager: un objeto que sabe entrar en un contexto (abrir el fichero) y salir de él limpiamente (cerrarlo), pase lo que pase entre medias.

Cómo funciona with por dentro

Cuando escribes:

with mi_objeto as x:
    hacer_cosas(x)

Python ejecuta esto:

  1. Llama a mi_objeto.__enter__() → el valor que devuelva se asigna a x.
  2. Ejecuta el bloque (hacer_cosas(x)).
  3. Llama a mi_objeto.__exit__(exc_type, exc_value, traceback)siempre, haya o no excepción.

Si hubo excepción dentro del bloque, los argumentos de __exit__ traen la info. Si __exit__ devuelve True, la excepción se considera manejada y no se propaga. Si devuelve False/None, se propaga.

Eso son los dunder methods del context manager: __enter__ y __exit__. Cualquier objeto que los implemente funciona con with.

Crear un context manager con clase

Caso típico: cronómetro que mide cuánto dura un bloque de código.

import time

class Cronometro:
    def __init__(self, etiqueta="Bloque"):
        self.etiqueta = etiqueta

    def __enter__(self):
        self.inicio = time.time()
        return self          # ← lo que se asigna a `as cron`

    def __exit__(self, exc_type, exc_value, traceback):
        duracion = time.time() - self.inicio
        print(f"{self.etiqueta} tardó {duracion:.4f}s")
        # No devolvemos True, así las excepciones se propagan normal


with Cronometro("descarga"):
    time.sleep(1)
# descarga tardó 1.0010s

Tres detalles:

  • __enter__ puede devolver self o cualquier otro objeto. Lo que devuelva queda asignado al as.
  • __exit__ se llama siempre al salir del bloque, exception o no.
  • Si quieres “tragar” la excepción, devuelve True desde __exit__. Casi nunca lo querrás.

📥 Llévate el cheatsheet de Python (gratis)

PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.

Crear un context manager con @contextmanager

La forma corta cuando no necesitas estado complejo. Usa el decorador @contextmanager de contextlib:

from contextlib import contextmanager
import time

@contextmanager
def cronometro(etiqueta="Bloque"):
    inicio = time.time()
    try:
        yield                          # ← aquí el bloque se ejecuta
    finally:
        print(f"{etiqueta} tardó {time.time() - inicio:.4f}s")


with cronometro("descarga"):
    time.sleep(1)

Sintaxis:

  • Antes del yield → setup (lo que hace __enter__).
  • El yield → entrega el control al bloque del with.
  • Después del yield (en un try/finally) → cleanup (lo que hace __exit__).

💡 ¿Te suena yield? Es literalmente la misma palabra de generadores Python. @contextmanager reutiliza el mecanismo de generador para definir un context manager con menos boilerplate.

Tip-friki: ¿Quieres pasar valor al as? Haz yield valor. Por ejemplo, en un context manager que abre BBDD: yield conexion.

Casos reales típicos

Cambiar al directorio temporal y volver

from contextlib import contextmanager
from pathlib import Path
import os

@contextmanager
def cd(ruta):
    """Cambia al directorio dado y vuelve al original al salir."""
    original = Path.cwd()
    os.chdir(ruta)
    try:
        yield
    finally:
        os.chdir(original)


with cd("/tmp"):
    # aquí estás en /tmp
    print(Path.cwd())
# aquí ya estás de vuelta en el directorio original, pase lo que pase

Bloquear un recurso temporalmente

import threading

lock = threading.Lock()

with lock:
    # acceso exclusivo
    actualizar_contador()
# lock.release() automático aunque falle dentro

Lock y RLock ya implementan el protocolo. Lo mismo para Semaphore, Event, etc.

Suprimir un tipo de excepción

from contextlib import suppress
from pathlib import Path

with suppress(FileNotFoundError):
    Path("temporal.txt").unlink()
# si el fichero no existe, no pasa nada — sin try/except

suppress es un context manager nativo de contextlib para “ignorar X excepción concreta”. Útil cuando borras temporales que pueden no existir.

Multiple context managers en una línea

with open("origen.txt") as src, open("destino.txt", "w") as dst:
    dst.write(src.read())

Equivalente a anidar dos with. Más limpio.

Test con mock temporal

from unittest.mock import patch

with patch("requests.get") as mock_get:
    mock_get.return_value.status_code = 200
    resultado = mi_funcion()
# mock se restaura solo al salir

patch de unittest.mock es un context manager. Cambia la función real durante el bloque y la restaura al salir. Patrón clásico en tests.

Transacción de BBDD con rollback automático

from contextlib import contextmanager

@contextmanager
def transaccion(conexion):
    try:
        yield
        conexion.commit()
    except Exception:
        conexion.rollback()
        raise

with transaccion(conn):
    conn.execute("UPDATE usuarios SET activo = 0 WHERE id = ?", (5,))
    conn.execute("INSERT INTO log (...) VALUES (...)")
# si todo OK → commit. Si excepción → rollback automático.

¿Y si necesito with reentrante o stacked?

Caso menos común pero útil: aplicar varios context managers dinámicamente.

from contextlib import ExitStack

def procesar(rutas):
    with ExitStack() as stack:
        ficheros = [stack.enter_context(open(r)) for r in rutas]
        # todos los ficheros abiertos. Se cerrarán al salir.
        for f in ficheros:
            print(f.read())

ExitStack te deja apilar context managers en runtime. Útil cuando el número no es fijo.

Errores típicos al usar context managers

# 1. No usar `with` cuando claramente toca
f = open("datos.txt")          # ❌ y si lanza excepción y olvidas close()?
contenido = f.read()
f.close()

# Mejor:
with open("datos.txt") as f:    # ✓
    contenido = f.read()

# 2. Olvidar el try/finally en @contextmanager
@contextmanager
def malo():
    print("Setup")
    yield                        # ❌ si el bloque del with lanza, "Cleanup" NO corre
    print("Cleanup")

@contextmanager
def bien():
    print("Setup")
    try:
        yield
    finally:
        print("Cleanup")          # ✓ corre siempre

# 3. Usar __init__ para abrir recursos
class MalConexion:
    def __init__(self, ruta):
        self.f = open(ruta)      # ❌ se abre al instanciar, no al hacer with

# Mejor abrir en __enter__:
class BienConexion:
    def __init__(self, ruta):
        self.ruta = ruta

    def __enter__(self):
        self.f = open(self.ruta)
        return self.f

    def __exit__(self, *args):
        self.f.close()

# 4. Devolver self por inercia cuando deberías devolver otra cosa
class Cronometro:
    def __enter__(self):
        return self     # OK si vas a usar `cron.duracion` o así

# Si no usas el `as`, ni te molestes con el return.

Resumen

Concepto Regla
Sintaxis with mi_objeto as x: ...
Protocolo __enter__ (setup) y __exit__ (cleanup, siempre)
Forma corta @contextmanager con try/yield/finally
Múltiples a la vez with a as x, b as y: ...
Apilar dinámicamente ExitStack().enter_context(cm)
Suprimir excepción concreta from contextlib import suppress
Cuándo usarlos Recursos a cerrar/restaurar/limpiar de forma garantizada

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.

Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.

Tu siguiente paso

Si has llegado hasta aquí, los context managers ya no son magia para ti — son una herramienta concreta para garantizar limpieza, pase lo que pase. La próxima vez que veas un patrón try/finally repetido tres veces en tu código, ahí tienes un context manager esperándote. Y cuando crees el tuyo con @contextmanager, vas a sonreír por lo limpio que queda.

Si quieres aprender Python desde la base hasta proyectos reales con manejo de recursos, BBDD y APIs, en El Pythonista lo enseño paso a paso.

Un abrazo,
Oscar

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 35+ lecciones, código revisado, ejercicios y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

35+ lecciones · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario