`with open(…)`: cierre garantizado de archivos en Python

Cuando abres un archivo con f = open("datos.txt"), Python te devuelve un objeto de archivo y el sistema operativo reserva un descriptor de archivo —un recurso limitado del kernel. Si tu código lanza una excepción antes de llegar a f.close(), ese descriptor queda abierto indefinidamente. En un script de uso único esto es molesto; en un servidor que procesa miles de peticiones, es una bomba de tiempo: el proceso eventualmente falla con OSError: [Errno 24] Too many open files.

El bloque with resuelve esto con lo que se llama un context manager: un protocolo que garantiza la ejecución de código de limpieza siempre, sin importar cómo salgas del bloque —ya sea normalmente, con un return, o con una excepción.

El mecanismo concreto son dos métodos especiales. Cuando Python encuentra with open("datos.txt") as f:, llama a open().__enter__(), que devuelve el objeto de archivo y lo asigna a f. Cuando el bloque termina —por cualquier razón— Python llama automáticamente a f.__exit__(exc_type, exc_val, exc_tb), que internamente ejecuta f.close(). Si hubo excepción, los argumentos la describen; si todo fue bien, los tres son None. En ambos casos, close() ocurre.

Usa with siempre que abras un archivo. La pregunta no es “¿es necesario aquí?”, sino “¿por qué arriesgarme a no cerrarlo?”. La única excepción razonable es en el REPL explorando datos a mano.

import sys

def contar_palabras(ruta: str) -> int:
    total = 0
    with open(ruta, encoding="utf-8") as f:
        for linea in f:
            total += len(linea.split())
    return total


def copiar_filtrando_vacias(origen: str, destino: str) -> int:
    """Copia líneas no vacías de origen a destino.
    Devuelve cuántas líneas se escribieron.
    """
    escritas = 0

    # Dos archivos en un solo with: ambos se cierran aunque falle cualquiera.
    with open(origen, encoding="utf-8") as src, open(destino, "w", encoding="utf-8") as dst:
        for linea in src:
            if linea.strip():          # descarta líneas vacías o solo espacios
                dst.write(linea)
                escritas += 1

    return escritas


def demo_cierre_ante_excepcion(ruta: str) -> None:
    try:
        with open(ruta, encoding="utf-8") as f:
            print(f"¿Cerrado dentro del with? {f.closed}")   # False
            raise ValueError("algo salió mal a mitad de lectura")
    except ValueError:
        pass  # la excepción se propagó, pero __exit__ ya se ejecutó

    print(f"¿Cerrado después del with? {f.closed}")   # True — garantizado


if __name__ == "__main__":
    import tempfile, os

    # Crear archivo temporal para el demo
    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt",
                                     delete=False, encoding="utf-8") as tmp:
        tmp.write("hola mundo\n\nesta línea tiene texto\n   \nfin\n")
        ruta_tmp = tmp.name

    print("Palabras:", contar_palabras(ruta_tmp))

    ruta_dest = ruta_tmp + ".out"
    n = copiar_filtrando_vacias(ruta_tmp, ruta_dest)
    print(f"Líneas copiadas: {n}")

    demo_cierre_ante_excepcion(ruta_tmp)

    os.unlink(ruta_tmp)
    os.unlink(ruta_dest)

Qué está pasando en el código

contar_palabras es el patrón más básico: abres, iteras línea a línea (eficiente en memoria porque no carga el archivo entero), y el with cierra el archivo al salir. Nada especial, pero ese “nada especial” es exactamente el punto: no tienes que recordar cerrar.

copiar_filtrando_vacias muestra la sintaxis de múltiples context managers en un solo with: with open(a) as f1, open(b) as f2:. Esto es equivalente a anidar dos bloques with, pero más limpio. Si open(destino) falla —porque el directorio no existe, por ejemplo— Python igual cierra src porque su __exit__ ya fue registrado. El orden de cierre es inverso al de apertura: dst primero, luego src.

demo_cierre_ante_excepcion hace explícito el contrato: incluso cuando lanzas una excepción dentro del bloque, f.closed es True en cuanto sales. El atributo .closed está ahí precisamente para que puedas verificarlo; en código de producción no necesitas comprobarlo, pero es útil para entender qué garantiza el protocolo.

Fíjate en que f sigue siendo accesible fuera del with —Python no destruye la variable, solo cierra el descriptor. Intentar leer de f después del bloque lanzará ValueError: I/O operation on closed file, que es el comportamiento correcto: el recurso liberado ya no está disponible.

Errores que debes conocer

Error: Abrir el archivo fuera del with y confiar en que el GC lo cerrará eventualmente.

# ❌ Wrong
f = open("log.txt", "w")
f.write("entrada\n")
# si aquí lanza excepción, f nunca se cierra hasta que el GC actúe (o nunca)

# ✅ Right
with open("log.txt", "w") as f:
    f.write("entrada\n")

CPython cierra el archivo cuando el contador de referencias llega a cero, pero PyPy u otras implementaciones no garantizan cuándo ocurre eso; el with hace el cierre determinístico en cualquier implementación.

Error: Usar with pero leer el contenido después de salir del bloque.

# ❌ Wrong
with open("datos.txt") as f:
    pass  # olvidamos leer aquí
contenido = f.read()  # ValueError: I/O operation on closed file

# ✅ Right
with open("datos.txt") as f:
    contenido = f.read()
# ahora contenido está disponible fuera del bloque

El archivo se cierra al salir del with; todo el trabajo con el objeto f debe ocurrir dentro.

Error: Encadenar open() sin with al pasar archivos a funciones que pueden lanzar.

# ❌ Wrong
procesar(open("a.txt"), open("b.txt"))   # si open("b.txt") falla, "a.txt" queda abierto

# ✅ Right
with open("a.txt") as fa, open("b.txt") as fb:
    procesar(fa, fb)

La coma dentro del with registra cada context manager de forma independiente, así el cierre de fa está garantizado aunque open("b.txt") explote.

82

Dejar un comentario

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

Scroll al inicio