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.
| Escribiste | Reemplá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
- Creación exclusiva de archivos:
open(path, "x")es el gemelo deexist_okpara archivos — el modo'x'significa "exclusive creation" y falla conFileExistsErrorsi el archivo existe. Es la forma libre de carreras de implementar "crea este archivo solo si soy el primero" (lock files, marcadores de una sola vez). - El código generado por IA adora LBYL. Los asistentes de código, entrenados con décadas de ejemplos estilo C, emiten
if not os.path.exists(...)todo el tiempo. Agrega "prefiere EAFP; usaexist_ok=True/ modo'x'en lugar de chequeos de existencia" a tu checklist de revisión o a las instrucciones de tu agente — es un lint barato que atrapa bugs reales de concurrencia. - Piso de versión:
exist_okllegó en Python 3.2, y una peculiaridad conmodedistinto se eliminó en 3.4.1 — en cualquier Python soportado hoy, la línea única es segura. - Los chequeos siguen siendo válidos para reportar.
path.exists()es legítimo cuando solo necesitas mostrar estado ("cache encontrada, se omite la descarga"). El bug es actuar según la respuesta como si no pudiera cambiar.
Recursos
- os.makedirs — documentación de la biblioteca estándar de Python
- pathlib.Path.mkdir — semántica de parents y exist_ok
- EAFP — glosario de Python
- LBYL — glosario de Python (la advertencia sobre condiciones de carrera)
- CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
- open() built-in — el modo 'x' de creación exclusiva
¿Construyendo una funcionalidad con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.