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.
Contenido
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:
- Llama a
mi_objeto.__enter__()→ el valor que devuelva se asigna ax. - Ejecuta el bloque (
hacer_cosas(x)). - 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 devolverselfo cualquier otro objeto. Lo que devuelva queda asignado alas.__exit__se llama siempre al salir del bloque, exception o no.- Si quieres “tragar” la excepción, devuelve
Truedesde__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 delwith. - Después del
yield(en untry/finally) → cleanup (lo que hace__exit__).
💡 ¿Te suena
yield? Es literalmente la misma palabra de generadores Python.@contextmanagerreutiliza el mecanismo de generador para definir un context manager con menos boilerplate.⚡ Tip-friki: ¿Quieres pasar valor al
as? Hazyield 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.
35+ lecciones · Proyecto real · Acceso de por vida · 14 días de garantía
