How to check if a file exists.
pathlib. Or try/except.
Three ways to check: pathlib (modern, recommended), os.path (legacy), and try/except (race-condition-free). The choice matters. Checking a file's existence and then opening it is a race condition — another process can delete the file between the check and the open. The right pattern for "I need this file to exist" is to try to open it and handle the FileNotFoundError. This page covers each method, the race condition, and the patterns that actually work.
Short answer: Use Path('file.txt').exists() from pathlib to check if a path exists. Use .is_file() for a file specifically, .is_dir() for a directory. Use try/except FileNotFoundError when you're about to open the file — it's race-condition-free. Avoid if os.path.exists(): open() — between the check and the open, the file can be deleted. In 2026, use pathlib for checks; use try/except for opening.
✅ The three methods
pathlib, os.path, and try/except.
from pathlib import Path
import os.path
file = Path('config.json')
# ✅ pathlib — modern, recommended
if file.exists():
print('Path exists')
if file.is_file():
print('It is a file')
if file.is_dir():
print('It is a directory')
# ✅ os.path — legacy, still works
if os.path.exists('config.json'):
print('Path exists')
if os.path.isfile('config.json'):
print('It is a file')
if os.path.isdir('some_dir'):
print('It is a directory')
# ✅ try/except — race-condition-free for opening
try:
with open('config.json') as f:
content = f.read()
print('Read successfully')
except FileNotFoundError:
print('File does not exist')
except PermissionError:
print('Permission denied')
# Compare the two approaches:
# ❌ Race condition — file can disappear between
# exists() and open()
if os.path.exists('config.json'):
with open('config.json') as f: # may raise FileNotFoundError
content = f.read()
# ✅ No race — the open is the only operation
try:
with open('config.json') as f:
content = f.read()
except FileNotFoundError:
content = None # or default
The three methods compared:
pathlib:
✅ Modern (Python 3.4+)
✅ Object-oriented: Path('file').exists()
✅ Chains nicely: Path('dir').joinpath('file')
✅ Reads like English: file.is_file()
✅ Cross-platform: no os.sep or \\ issues
✅ More methods: read_text, write_text, glob, rglob
❌ Not available in very old Python (pre-3.4)
os.path:
✅ Works everywhere (pre-3.4 too)
✅ Familiar to old-timers
✅ Slightly faster for very large loops (no object)
❌ Function-based, less discoverable
❌ Verbose: os.path.isfile(os.path.join(a, b))
❌ Mixes strings and path handling
try/except:
✅ Race-condition-free
✅ Handles the actual failure mode (open)
✅ One operation instead of two
❌ Doesn't tell you WHY it failed without catching
multiple exceptions (FileNotFoundError,
PermissionError, IsADirectoryError, OSError)
❌ Verbose for "I just want to know if it exists"
Which to use:
✅ To check existence (e.g., "is this config file present?"):
pathlib.Path('file').exists()
✅ To check type (file vs directory):
pathlib.Path('file').is_file()
pathlib.Path('file').is_dir()
✅ Before opening a file:
try/except FileNotFoundError — the pattern
that actually works.
❌ Don't do:
if Path('file').exists():
Path('file').read_text()
→ The file can be deleted between the two.
✅ Do:
try:
content = Path('file').read_text()
except FileNotFoundError:
content = ''
→ The read is the check. No race.
The race condition is real:
Time Process A Process B
──── ───────── ─────────
t=0 exists('file') → True
t=1 delete('file')
t=2 open('file') → FileNotFoundError!
The exists() check returned True, but by
the time open() runs, the file is gone.
The check was useless.
Fix: just try to open. If it fails, handle
the error. The failure and the handling are
separated by microseconds, not by an intermediate
check.
Or: if you really need a check (for logging,
for deciding whether to create a default file),
the check is informational. The actual file
operation still needs try/except.
The pattern:
try:
# do the file operation
except FileNotFoundError:
# handle "doesn't exist"
except PermissionError:
# handle "exists but no access"
except IsADirectoryError:
# handle "it's a directory, not a file"
except OSError:
# catch-all for other OS errors
The specific exception tells you what happened.
The generic OSError catches the rest.
For "I need to know if it exists":
exists = Path('file').exists()
→ This is fine for logging, for reporting,
for the "check" part of "check then maybe create."
For "I need to read this file":
try:
with open('file') as f:
...
except FileNotFoundError:
...
→ No race. Correct.
✅ The race condition
Why check-then-open is wrong.
from pathlib import Path
# ❌ The buggy pattern — check then open
file = Path('data.txt')
if file.exists():
# Between here and the next line, the file could
# be deleted, renamed, or moved by another process.
content = file.read_text() # may raise FileNotFoundError
print(content)
# Why this matters:
# Scenario 1: log rotation
# - App checks if log.txt exists → True
# - Logrotate deletes log.txt and creates a new one
# - App reads log.txt → FileNotFoundError
# Scenario 2: temp files
# - App checks if /tmp/output-1234 exists → True
# - Another instance of the app deletes it as cleanup
# - App opens it → FileNotFoundError
# Scenario 3: network shares
# - App checks if /mnt/share/data exists → True
# - Network blip, file becomes unavailable
# - App opens it → FileNotFoundError or OSError
# ✅ The correct pattern — try to open, handle failure
try:
content = file.read_text()
print(content)
except FileNotFoundError:
print('File does not exist')
# Create default, or exit, or skip
except PermissionError:
print('No permission to read')
except IsADirectoryError:
print('Path is a directory, not a file')
except OSError as e:
print(f'OS error: {e}')
# ✅ If you need to know whether the file existed
# (for logging, for creating if missing), do
# the try/except and set a flag:
file_existed = True
try:
content = file.read_text()
except FileNotFoundError:
file_existed = False
content = ''
# optionally create it
file.write_text('default')
if file_existed:
print('File was already there')
else:
print('File was created with defaults')
# The pattern: try the operation, handle the failure.
# Don't check first, don't open second. The check
# is the open.
Why check-then-open is a race:
Thread A Thread B
───────── ─────────
exists('file') → True
delete('file')
open('file') → FileNotFoundError
Between the check and the open, the world
changed. The check result is stale.
The bug is subtle:
- Most of the time it works.
- Rarely, under load or with concurrent
processes, it fails.
- The failure is in the log at 3 AM.
- Reproducing is hard.
- Fixing is easy (try/except), but only
if you know the pattern.
This is a classic "time-of-check to time-of-use"
(TOCTOU) bug. It's not Python-specific — it's
a general concurrency issue.
The fix: don't check. Do the thing and handle
the failure. This is atomic-ish —
- The file is either there when you open it,
or it's not.
- The exception is definitive.
- No intermediate state to be wrong about.
When checking is still okay:
The existence check is fine when:
→ The result is informational (logging,
displaying to the user).
→ You're not acting on it in a way that
assumes the state will persist.
→ You handle the failure of the subsequent
operation anyway.
Example of acceptable checking:
if not Path('config.json').exists():
print('Warning: no config file found, '
'using defaults')
try:
content = Path('config.json').read_text()
except FileNotFoundError:
content = DEFAULT_CONFIG
The check is for the warning.
The try/except is for correctness.
Both can be true.
The rule:
Use exists() for information.
Use try/except for control flow.
Never use exists() to guard an operation
that assumes the file is still there.
Real-world example:
❌ def load_config():
if not Path('config.json').exists():
raise ConfigError('No config')
return json.loads(Path('config.json').read_text())
Between exists() and read_text(), the file
could disappear.
✅ def load_config():
try:
return json.loads(Path('config.json').read_text())
except FileNotFoundError:
raise ConfigError('No config')
except json.JSONDecodeError:
raise ConfigError('Malformed config')
The read is the check. Two operations become
one try/except block. Cleaner and correct.
This pattern also handles the "file exists but
is malformed" case — the try/except can catch
multiple exceptions, telling you exactly what
went wrong.
✅ Common patterns
What you'll actually use.
from pathlib import Path
import os
# 1. Create a directory if it doesn't exist
Path('output').mkdir(parents=True, exist_ok=True)
# No check needed — mkdir handles both cases
# exist_ok=True: no error if it already exists
# parents=True: create intermediate dirs
# 2. Write a file, creating parents
Path('output/data.txt').parent.mkdir(parents=True, exist_ok=True)
Path('output/data.txt').write_text('content')
# Or: just write — Python creates parents on write?
# No, it doesn't. Create parents explicitly.
# 3. Read a file with a default if missing
def read_or_default(path, default=''):
try:
return Path(path).read_text()
except FileNotFoundError:
return default
content = read_or_default('config.json', '{}')
# 4. Check if a directory is empty
def is_empty_dir(path):
p = Path(path)
return p.is_dir() and not any(p.iterdir())
# 5. Create a file only if it doesn't exist (atomic)
def write_if_missing(path, content):
try:
with open(path, 'x') as f: # 'x' = exclusive creation
f.write(content)
return True
except FileExistsError:
return False
# 6. List files in a directory (if it exists)
def safe_listdir(path):
p = Path(path)
if not p.is_dir():
return []
return [f.name for f in p.iterdir()]
# 7. Delete a file if it exists
def delete_if_exists(path):
try:
Path(path).unlink()
return True
except FileNotFoundError:
return False
# 8. Get file size, handling missing file
def safe_size(path):
try:
return Path(path).stat().st_size
except FileNotFoundError:
return 0
# 9. Check if a path is a symlink to something
def check_symlink(path):
p = Path(path)
return p.is_symlink(), p.resolve() if p.exists() else None
# 10. Glob files with existence check
def find_files(pattern, root='.'):
root_path = Path(root)
if not root_path.exists():
return []
return list(root_path.glob(pattern))