Aprende leyendo en orden

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.

Olvidar cerrar también retiene los recursos del proceso externo
procesoPythonabreconexiónMySQL / SOretiene recursoclose()libera ambos ✅Python(terminó)conexiónsigue abiertaMySQL / SOsigue reteniendorecursoFD agotados /límite deconexiones ❌
Los archivos y BDs entregan un recurso a el SO u otro proceso fuera de Python. Si Python no llama 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.

Flujo de ejecución de with X() as y:
with X() as y:entra__enter__llamadoel valor de retornova a ycuerpo delbloque__exit__limpiezareturnsale
Al entrar, __enter__ corre y su valor de retorno va a la variable nombrada tras as. Al salir — normal o excepcional — __exit__ siempre se llama para la limpieza.

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.

Escribe el mismo DatabaseManager del ejemplo y maneja con with.

① Define class DatabaseManager: y asigna self.db_name = db_name y self.connection = None en __init__(self, db_name).

② Define __enter__(self). Imprime f"Conectando a {self.db_name}", fija self.connection = f"connection_to_{self.db_name}", luego return self.connection.

③ Define __exit__(self, exc_type, exc_val, traceback). Imprime f"Desconectando de {self.db_name}", fija self.connection = None y por último return False.

④ Dentro de with DatabaseManager("user_data_db") as conn:, imprime f"conexión activa: {conn}" y luego print("insertando datos").

(Si tu código se ejecuta correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

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ónexc_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.

Valores concretos entregados a los 3 argumentos de __exit__
qué pasóen el withexc_typeexc_valtracebacksalida normal(sin excepción)NoneNoneNoneraiseValueError("invalid")<class'ValueError'>ValueError('invalid')<tracebackobject>
Salida normal entrega None × 3. Excepciones entregan la tripla «clase / instancia / traceback». Un raise ValueError("invalid") concreto hace tangibles los contenidos.
Salida normal vs salida por excepción en __exit__
salida normaldel withexc_type =None, etc.__exit__solo limpiezasale delbloque withexcepcióndentro del withexc_type / val/ traceback__exit__log + limpiezareturn False-> propaga
Cuando el bloque lanza, la información de la excepción se empaca en los tres argumentos de __exit__. El valor de retorno elige tragar (True) o relanzar (False).

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__.

Modifica el DatabaseManager de la Práctica 1 para confirmar dos cosas: __exit__ corre incluso ante excepciones y sus tres argumentos reciben valores.

① Define class DatabaseManager: y asigna self.db_name = db_name en __init__(self, db_name).

② En __enter__(self), imprime f"Enter: {self.db_name}" y return self (para que as enlace al manager mismo).

③ En __exit__(self, exc_type, exc_val, traceback), imprime los tres argumentos línea por línea (print("exc_type:", exc_type) y así), luego print(f"Exit: {self.db_name}"), y por último return False.

④ Dentro de un bloque try:, abre with DatabaseManager("shop_db"):, imprime "start", luego raise ValueError("bad inventory data").

⑤ Captura con except ValueError as e: y print(f"caught outside: {e}").

Editor Python

Ejecutar el código para ver el resultado

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
with concentra la responsabilidad de apertura/cierre en la clase
Si vas con try / finally
  • Llamador — tiene que escribir try / finally cada vez
  • Olvidos — un mal copiar y pegar y aparece una fuga
  • Coste de cambio — pasos extra de limpieza significan editar cada sitio de llamada
sentencia with + context manager
  • 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__
Separar «el usuario del recurso» del «dueño de apertura/cierre» es el valor que añade 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.

QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1En with X() as y:, el valor enlazado a y es el valor de retorno de qué método?

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?