La sentencia with de Python y los gestores de contexto
Aprende cómo funcionan la sentencia with y los gestores de contexto en Python, cómo escribir los tuyos con __enter__/__exit__ y cómo usar contextlib.
La sentencia with garantiza que los recursos como archivos, conexiones de red y bloqueos se configuren y limpien correctamente, incluso cuando una excepción interrumpe el bloque. El objeto que controla esta configuración y desmontaje se llama gestor de contexto.
Este capítulo explica cómo funciona la sentencia with, cuándo usarla, cómo escribir tus propios gestores de contexto con __enter__ y __exit__, y cómo crear versiones ligeras con contextlib.contextmanager.
Por qué existe with
Antes de la sentencia with, la gestión de recursos implicaba escribir bloques try/finally a mano:
f = open("data.txt", "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close() # must always close, even if read() raisesEsto funciona, pero es verboso, fácil de olvidar y añade código repetitivo alrededor de cada recurso. La sentencia with condensa esto en un bloque único y legible, y gestiona la limpieza automáticamente:
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f is closed here, no matter what happened inside the blockLa cláusula as f enlaza el valor del gestor de contexto al nombre f. Algunos gestores de contexto no producen un valor útil; en ese caso puedes omitir as:
with some_lock:
shared_data.append(item)Cómo funciona la sentencia with
Cuando Python ejecuta una sentencia with, sigue esta secuencia:
- Evalúa la expresión después de
with— esto produce el objeto gestor de contexto. - Llama al método
__enter__()del gestor de contexto. El valor de retorno de__enter__()se enlaza a la variableas(si está presente). - Ejecuta el cuerpo del bloque
with. - Llama al método
__exit__(exc_type, exc_val, exc_tb)del gestor de contexto.- Si el bloque se completó normalmente, los tres argumentos son
None. - Si se lanzó una excepción, los tres argumentos la describen.
- Si
__exit__devuelve un valor verdadero, la excepción se suprime y la ejecución continúa después del bloquewith. Si devuelve un valor falso (oNone), la excepción se propaga.
- Si el bloque se completó normalmente, los tres argumentos son
Este protocolo se denomina el protocolo de gestor de contexto.
Abrir archivos con with
El uso más común de with es el manejo de archivos. Los objetos de archivo integrados de Python implementan el protocolo de gestor de contexto, por lo que se cierran automáticamente cuando termina el bloque:
with open("report.txt", "w", encoding="utf-8") as f:
f.write("Sales: 1 000\n")
f.write("Returns: 23\n")
print(f.closed) # True — file was closed on exitSi ocurre una excepción dentro del bloque, el archivo sigue cerrándose:
try:
with open("data.txt", "r", encoding="utf-8") as f:
raise RuntimeError("something went wrong")
except RuntimeError:
pass
print(f.closed) # True — closed despite the exceptionSin with, olvidar f.close() después de un error deja el descriptor de archivo abierto hasta que se ejecute el recolector de basura, o hasta que el proceso termine, lo que puede causar pérdida de datos o errores de "demasiados archivos abiertos" en programas de larga duración.
Abrir múltiples recursos a la vez
Puedes abrir varios recursos en una sola sentencia with separándolos con comas (Python 3.1+):
with open("input.txt", "r", encoding="utf-8") as src, \
open("output.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())Esto es exactamente equivalente a anidar dos sentencias with, pero mantiene el nivel de sangría plano.
Escribir un gestor de contexto con __enter__ y __exit__
Cualquier clase que defina __enter__ y __exit__ puede usarse con la sentencia with. Aquí hay un ejemplo mínimo — un temporizador que mide cuánto tiempo tarda en ejecutarse el bloque with:
import time
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
# Elapsed: 0.0xxx s
print(total) # 499999500000Puntos clave:
__enter__se ejecuta antes del bloque. Devuelve el valor enlazado aas t. Devolverselfpermite al llamador acceder at.elapsedy otros atributos si es necesario.__exit__se ejecuta después del bloque, incluso en caso de excepción. DevolverFalse(oNone) permite que cualquier excepción se propague normalmente.
Suprimir excepciones en __exit__
Si __exit__ devuelve True, la excepción se silencia y la ejecución continúa después del bloque with. Esto es intencional en contextos específicos — por ejemplo, un gestor de contexto que captura y registra errores sin interrumpir el programa:
class Ignore:
"""Silently ignore any exception raised inside the with block."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Suppressed: {exc_type.__name__}: {exc_val}")
return True # suppress the exception
with Ignore():
x = 1 / 0 # ZeroDivisionError is caught and ignored
print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues hereUsa la supresión de excepciones con cuidado — silenciar errores puede ocultar errores en el código. contextlib.suppress de la biblioteca estándar es la forma idiomática de hacer esto (véase más abajo).
Un gestor de contexto para conexiones de base de datos
Un ejemplo más realista — gestionar una conexión estilo base de datos que confirma en caso de éxito y revierte en caso de error:
class ManagedTransaction:
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.begin()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # always let exceptions propagateEl patrón — confirmar en éxito, revertir en fallo — aparece en las bibliotecas de bases de datos reales (SQLite, SQLAlchemy y psycopg2 lo implementan todos).
contextlib.contextmanager: gestores de contexto basados en generadores
Escribir una clase completa con __enter__ y __exit__ es el enfoque correcto para gestores de contexto complejos o con estado. Para casos más simples, el decorador contextlib.contextmanager permite expresar la misma lógica como una función generadora:
from contextlib import contextmanager
@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
print(f"Opening {path}")
f = open(path, mode, encoding=encoding)
try:
yield f # everything up to yield is __enter__
finally:
f.close() # everything after yield is __exit__
print(f"Closed {path}")
with managed_open("notes.txt", "w") as f:
f.write("hello\n")
# Opening notes.txt
# Closed notes.txtEl protocolo del generador se mapea directamente sobre el protocolo del gestor de contexto:
- El código antes de
yield→__enter__(configuración). - La expresión
yield→ el valor enlazado a la variableas. - El código después de
yield(normalmente en unfinally) →__exit__(desmontaje).
El try/finally alrededor de yield es importante: sin él, una excepción dentro del bloque with haría que el código de desmontaje nunca se ejecutara.
Ejemplo de contextmanager: directorio de trabajo temporal
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
original = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original)
with working_directory("/tmp"):
print(os.getcwd()) # /tmp (or system temp dir)
print(os.getcwd()) # restored to original directoryEste patrón también está disponible en la biblioteca estándar como tempfile.TemporaryDirectory.
Utilidades de contextlib
El módulo contextlib incluye varios gestores de contexto predefinidos que vale la pena conocer:
contextlib.suppress
Suprime excepciones específicas sin ningún código repetitivo:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error even if file does not existEquivalente a un try/except que no hace nada cuando captura la excepción.
contextlib.nullcontext
Un gestor de contexto sin operación, útil cuando condicionalmente quieres usar un gestor de contexto o no:
from contextlib import nullcontext
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return sorted(data)Sin nullcontext, necesitarías una rama if lock: cada vez.
contextlib.ExitStack
ExitStack permite gestionar un número dinámico de gestores de contexto — útil cuando el número de recursos no se conoce hasta el momento de ejecución:
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "w", encoding="utf-8"))
for f in files
]
for i, fh in enumerate(handles):
fh.write(f"file {i}\n")
# All three files are closed hereExitStack también es la herramienta adecuada cuando necesitas añadir condicionalmente un gestor de contexto, o cuando quieres diferir la limpieza a un punto posterior.
Cuándo usar with frente a try/finally
Usa with siempre que:
- Un recurso deba liberarse después de su uso (archivos, sockets, bloqueos, cursores de base de datos).
- Quieras garantizar la limpieza incluso en caso de excepciones.
- La lógica de limpieza sea siempre la misma independientemente del éxito o el fracaso.
Usa un try/finally simple solo cuando:
- Necesites acciones de limpieza diferentes según el tipo de excepción — aunque
__exit__también puede hacer esto. - Estés escribiendo código compatible con Python 2 (poco frecuente hoy en día).
En la práctica, si el objeto admite el protocolo de gestor de contexto, siempre prefiere with.
Referencia rápida
| Característica | Qué hace |
|---|---|
with expr as v: | Llama a expr.__enter__(), enlaza el resultado a v, llama a __exit__ al salir |
| Múltiples recursos | with A() as a, B() as b: — ambos se limpian aunque B() lance una excepción |
__enter__(self) | Configuración; el valor de retorno se enlaza a la variable as |
__exit__(self, exc_type, exc_val, exc_tb) | Desmontaje; devuelve True para suprimir la excepción |
@contextmanager | Convierte una función generadora en un gestor de contexto |
contextlib.suppress(E) | Silencia el tipo de excepción E sin un try/except |
contextlib.nullcontext() | Marcador de posición cuando un gestor de contexto es opcional |
contextlib.ExitStack | Gestiona un conjunto dinámico o condicional de gestores de contexto |
Capítulos relacionados
- Manejo de archivos en Python — usar
with open(...)para cada operación con archivos - Try Except en Python — manejo de excepciones y try/finally
- Decoradores en Python —
@contextmanagerusa el mismo patrón de decorador - Generadores en Python — cómo el protocolo de generadores impulsa
@contextmanager - Clases y objetos en Python — escribir métodos
__enter__y__exit__