That Innocent if exists Check Is a Race Condition
That innocent-looking if exists check is a race condition. Between the moment your code checks whether a directory exists and the moment it acts on the answer, another process — or another thread, another container replica, another CI job on the same runner — can create it. Your code then crashes on the very case you thought you handled.
The bug, in four lines
import os
if not os.path.exists(path): # check
os.makedirs(path) # act — but the world changed in between
If a second process creates path after your check and before your makedirs, the call raises FileExistsError. The check didn't prevent the failure; it just made the failure rare enough to reach production. Security folks have a name for this class of bug: CWE-367, Time-of-check Time-of-use (TOCTOU) — "the resource's state can change between the check and the use in a way that invalidates the results of the check."
The fix, in one line
os.makedirs(path, exist_ok=True)
With exist_ok=True (available since Python 3.2), an already-existing target directory is simply not an error — the docs are explicit: "If exist_ok is False (the default), a FileExistsError is raised if the target directory already exists." The underlying mkdir system call either creates the directory or reports it exists — there is no check-then-act gap left in your code for another process to slip into. You deleted a line and a bug.
The pathlib spelling is just as clean:
from pathlib import Path
Path(path).mkdir(parents=True, exist_ok=True)
parents=True creates missing ancestors (like mkdir -p); exist_ok=True suppresses FileExistsError — unless the path exists and is not a directory, in which case it still raises. That's a caveat worth knowing: a stray file named like your directory will still fail loudly, which is exactly what you want.
LBYL vs. EAFP
The check-then-act pattern has a name: LBYL — "look before you leap." Python's glossary warns about it directly: "In a multi-threaded environment, the LBYL approach can risk introducing a race condition between 'the looking' and 'the leaping.'" The idiomatic alternative is EAFP — "easier to ask for forgiveness than permission": attempt the operation and handle the failure, instead of predicting it.
| You wrote | Replace with |
|---|---|
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 or d.get(key) |
if os.path.exists(f): open(f) | try: open(f) except FileNotFoundError |
| "does the file NOT exist? then create it" | open(f, "x") — exclusive creation, fails if it exists |
The rule generalizes: when a built-in already encodes the fallback, let it do the work. dict.get, dict.setdefault, getattr(obj, name, default), contextlib.suppress — each replaces a check-then-act pair with a single atomic-from-your-code's-view operation.
Power-user notes
- Exclusive file creation:
open(path, "x")is the file-side twin ofexist_ok— the'x'mode means "exclusive creation," failing withFileExistsErrorif the file exists. It's the race-free way to implement "create this file only if I'm first" (lock files, one-time markers). - AI-generated code loves LBYL. Coding assistants trained on decades of C-style examples emit
if not os.path.exists(...)constantly. Add "prefer EAFP; useexist_ok=True/'x'mode instead of existence checks" to your review checklist or agent instructions — it's a cheap lint that catches real concurrency bugs. - Version floor:
exist_oklanded in Python 3.2, and a mode-mismatch quirk was removed in 3.4.1 — on any supported Python today, the one-liner is safe. - Checks are still fine for reporting.
path.exists()is legitimate when you only need to display state ("cache found, skipping download"). The bug is acting on the answer as if it can't change.
Resources
- os.makedirs — Python standard library docs
- pathlib.Path.mkdir — parents and exist_ok semantics
- EAFP — Python glossary
- LBYL — Python glossary (the race-condition warning)
- CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
- open() built-in — the 'x' exclusive-creation mode
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.