Pregunta 1En with X() as y:, el valor enlazado a y es el valor de retorno de qué método?
Sentencia with y context managers — Apertura/cierre seguro con __enter__ / __exit__
Aprende la sentencia with y los context managers de Python. El par __enter__ / __exit__ para apertura/cierre seguro y cómo escribir el tuyo — práctica incluida.
La última vez trabajamos en proteger el estado interno de una clase. Esta vez damos un paso hacia fuera — hacia los recursos externos que viven fuera del proceso de Python (archivos, conexiones a base de datos, sockets de red, locks) — y miramos la sentencia with y los context managers que manejan su adquisición y liberación de forma segura.
Por qué necesitas la sentencia with
Operaciones como abrir un archivo o conectarse a una base de datos vienen con un deber de limpieza: «cuando termines, ciérralo». Olvidar cerrar fuga descriptores de archivo, deja conexiones a la BD abiertas para siempre y deja al proceso externo aferrado a los recursos también.
close(), el otro lado sigue esperando instrucciones y reteniendo el recurso.Puedes escribir la misma lógica con try / finally, pero entonces cada autor tiene que recordar llamar close() dentro de finally cada vez. A medida que la base de código crece o más gente la toca, alguien lo olvidará — esa es la realidad.
La sentencia with encierra adquisición y liberación en una sola unidad sintáctica y las automatiza. with open("file.txt") as f: es el ejemplo canónico: se garantiza que el archivo se cierre en el momento en que sales del bloque with.
Escribir tu propio context manager — __enter__ y __exit__
Un objeto que se puede usar con with se llama context manager. Para convertir una clase en uno, basta con implementar dos métodos especiales.
- __enter__(self) — corre cuando entras al bloque with. Su valor de retorno se enlaza a la variable nombrada tras as.
- __exit__(self, exc_type, exc_val, traceback) — corre cuando sales del bloque. Siempre se llama, salida normal o excepcional.
Debajo hay un ejemplo mínimo estilo conexión a BD (no usamos realmente una librería de BD — la imitamos con cadenas).
class DatabaseManager:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None # aún no conectado
def __enter__(self):
print(f"Conectando a {self.db_name}")
self.connection = f"connection_to_{self.db_name}" # código real: objeto conexión real
return self.connection # valor enlazado a la variable as
def __exit__(self, exc_type, exc_val, traceback):
print(f"Desconectando de {self.db_name}")
self.connection = None # limpieza
return False # no tragarse las excepciones
with DatabaseManager("user_data_db") as conn:
print(f" conexión activa: {conn}")
print(" insertando datos")
# ↑ cuando este bloque sale, corre __exit__
Ejecútalo y la salida aparece en el orden «conectar → trabajo en el bloque → desconectar». La desconexión corre sin que nadie la llame explícitamente — ese es todo el valor de with. El desarrollador queda libre de preocuparse por cerrar la conexión.
Los tres argumentos de __exit__ — capturando excepciones
__exit__ toma tres argumentos: exc_type, exc_val, traceback. Python los usa para decirle a __exit__ si ocurrió una excepción dentro del bloque with.
- Salida normal — los tres son None. Solo limpia.
- Salida por excepción — exc_type es la clase de excepción, exc_val es la instancia, traceback es el objeto traceback.
El valor de retorno de __exit__ también tiene significado. Devolver True se traga la excepción — no se propaga más allá del bloque. Devolver False / None la relanza tras la limpieza. Por defecto debe ser False (o no return nada): registra o notifica, pero siempre deja que la excepción escape.
None × 3. Excepciones entregan la tripla «clase / instancia / traceback». Un raise ValueError("invalid") concreto hace tangibles los contenidos.Devolver True desde __exit__ mata silenciosamente la excepción
Si __exit__ devuelve True, la excepción dentro de with no se propaga. Es tentador, pero el llamador ahora cree que la operación tuvo éxito — eso es un efecto secundario serio. Por defecto usa False (o sin return): registra o notifica si quieres, pero siempre deja que la excepción burbujee hacia arriba.
Ahora dispara realmente una excepción dentro del with y observa lo que llega a los tres argumentos de __exit__.
Comparado con try / finally — Por qué gana with
El trabajo de un context manager se puede hacer con try / finally. La razón para elegir with en su lugar es que «el par de apertura/cierre vive dentro de la clase». Escribir la misma tarea de dos maneras hace obvia la diferencia en volumen y claridad del código del llamador.
# ❌ try / finally — el llamador escribe la limpieza a mano cada vez
db = DatabaseManager("shop_db")
conn = db.open() # método de conexión personalizado
try:
use(conn) # trabajo real
finally:
db.close() # no olvides — copiado y pegado por todas partes
# ✅ with — apertura/cierre vive en la clase, el llamador solo hace el trabajo
with DatabaseManager("shop_db") as conn:
use(conn) # no hace falta finally
- Llamador — tiene que escribir
try/finallycada vez - Olvidos — un mal copiar y pegar y aparece una fuga
- Coste de cambio — pasos extra de limpieza significan editar cada sitio de llamada
- Llamador — una línea,
with X() as y: - Olvidos — imposible a nivel de sintaxis (
__exit__siempre corre) - Coste de cambio — limpieza extra significa editar solo
__exit__
with. A medida que se multiplican los sitios de llamada, los cambios de limpieza siguen dentro de una sola clase.Usa with donde sea que adquisición y liberación se emparejen
Archivos, conexiones a BD, locks, sockets de red — donde sea que «agarres un recurso al inicio y debas devolverlo al final» — es candidato para with. La biblioteca estándar de Python ya expone muchos de estos como context managers: open(), threading.Lock(), sqlite3.connect(), etc.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Cuando se lanza una excepción dentro de un bloque with, ¿cuál describe correctamente el comportamiento de __exit__?
Pregunta 3Si __exit__ devuelve True, ¿qué pasa con la excepción lanzada dentro del bloque with?