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.
N° 82