Yeda AI Tips · #121

English

Ese inocente chequeo de if exists es una condición de carrera

Ese chequeo de if exists que parece inocente es una condición de carrera. Entre el momento en que tu código verifica si un directorio existe y el momento en que actúa según la respuesta, otro proceso — u otro hilo, otra réplica del contenedor, otro job de CI en el mismo runner — puede crearlo. Tu código entonces falla justo en el caso que creías haber manejado.

El bug, en cuatro líneas

import os

if not os.path.exists(path):   # check
    os.makedirs(path)          # act — but the world changed in between

Si un segundo proceso crea path después de tu chequeo y antes de tu makedirs, la llamada lanza FileExistsError. El chequeo no evitó la falla; solo la hizo lo bastante rara como para llegar a producción. En seguridad esta clase de bug tiene nombre: CWE-367, Time-of-check Time-of-use (TOCTOU) — "el estado del recurso puede cambiar entre el chequeo y el uso de una forma que invalida el resultado del chequeo".

La solución, en una línea

os.makedirs(path, exist_ok=True)

Con exist_ok=True (disponible desde Python 3.2), que el directorio destino ya exista simplemente no es un error — la documentación es explícita: "If exist_ok is False (the default), a FileExistsError is raised if the target directory already exists." La llamada al sistema mkdir subyacente crea el directorio o reporta que existe — ya no queda en tu código ninguna brecha chequear-y-actuar por donde otro proceso pueda colarse. Borraste una línea y un bug.

La versión con pathlib es igual de limpia:

from pathlib import Path

Path(path).mkdir(parents=True, exist_ok=True)

parents=True crea los ancestros faltantes (como mkdir -p); exist_ok=True suprime FileExistsError — salvo que la ruta exista y no sea un directorio, en cuyo caso igual lanza la excepción. Vale la pena conocer ese detalle: un archivo suelto con el nombre de tu directorio seguirá fallando de forma ruidosa, que es exactamente lo que quieres.

LBYL vs. EAFP

El patrón chequear-y-actuar tiene nombre: LBYL — "look before you leap" (mira antes de saltar). El glosario de Python lo advierte directamente: "In a multi-threaded environment, the LBYL approach can risk introducing a race condition between 'the looking' and 'the leaping.'" La alternativa idiomática es EAFP — "easier to ask for forgiveness than permission" (es más fácil pedir perdón que permiso): intenta la operación y maneja la falla, en lugar de predecirla.

EscribisteReemplázalo con
if not os.path.exists(p): os.makedirs(p)os.makedirs(p, exist_ok=True)
if not p.exists(): p.mkdir()p.mkdir(parents=True, exist_ok=True)
if key in d: v = d[key]try/except KeyError o d.get(key)
if os.path.exists(f): open(f)try: open(f) except FileNotFoundError
"¿el archivo NO existe? entonces créalo"open(f, "x") — creación exclusiva, falla si existe

La regla se generaliza: cuando un built-in ya codifica el fallback, deja que haga el trabajo. dict.get, dict.setdefault, getattr(obj, name, default), contextlib.suppress — cada uno reemplaza un par chequear-y-actuar con una sola operación atómica desde el punto de vista de tu código.

Notas para usuarios avanzados

Recursos

Read this article in English

¿Construyendo una funcionalidad con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.

Habla con nosotros · Lee el blog