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 Dino 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))
Don't use os.path.exists() to guard a file operation. This is the single most common file-handling bug in Python. if os.path.exists(f): open(f) works 99% of the time and fails at 3 AM under load. The fix is one line: try: open(f) except FileNotFoundError: .... The existence check is fine for informational purposes — displaying "file found" or logging "config missing" — but it's not a guard. The operation itself must handle the failure, because the file can disappear between any two operations.
⚠️ Common mistakes
The file-check mistakes that cost real time.
if exists(): open()Race condition. Fix: try: open() except FileNotFoundError.
exists() instead of is_file()Directories pass exists(). Fix: is_file() when you need a file.
os.path.exists with a Path objectWorks but redundant. Fix: use Path.exists() directly.
PermissionErrorThe file exists but you can't read it. Fix: catch both exceptions.
exists() follows symlinksIt does — for broken symlinks, exists() returns False. Fix: is_symlink() for the link itself.
os.listdir without a checkRaises if the directory doesn't exist. Fix: Path.iterdir() with a is_dir() check, or try/except.
parents=TrueFails if intermediate dirs don't exist. Fix: mkdir(parents=True, exist_ok=True).
FileNotFoundError. Fix: Path(path).parent.mkdir(parents=True, exist_ok=True) first.
❓ Frequently asked
How do I check if a file exists in Python?
Three methods. (1) pathlib (modern): from pathlib import Path; Path('file.txt').exists(). For a file specifically: .is_file(). (2) os.path (legacy): import os.path; os.path.isfile('file.txt'). (3) Try/except (race-condition-free): try: with open('file.txt') as f: ... except FileNotFoundError: ...
What is the difference between pathlib and os.path?
pathlib is the modern, object-oriented approach; os.path is the legacy, function-based approach. pathlib gives you Path objects with methods: Path('file.txt').exists(), .is_file(), .is_dir(). os.path gives you functions: os.path.exists(), os.path.isfile(). For new code, use pathlib.
Why is checking for a file then opening it a race condition?
Because between the check and the open, another process can delete the file, rename it, or move it. Example: if os.path.exists('config.json'): with open('config.json') as f: ... — between the exists() check and the open(), another process deletes config.json, and the open() raises FileNotFoundError anyway. Fix: just try to open the file.
How do I check if a directory exists?
pathlib: Path('mydir').is_dir() — True if it exists and is a directory. Or Path('mydir').exists() — True if anything exists at that path. os.path: os.path.isdir('mydir') or os.path.exists('mydir'). For creating a directory if it doesn't exist: Path('mydir').mkdir(exist_ok=True).
How do I check if a path is a file or a directory?
pathlib: Path(p).is_file() — True only if it's a file. Path(p).is_dir() — True only if it's a directory. Path(p).exists() — True if anything exists. os.path: os.path.isfile(p), os.path.isdir(p), os.path.exists(p). Note: symlinks. Path.is_file() and .is_dir() follow symlinks.
What about checking for a file on a network share?
The same methods work, but network shares have additional latency and reliability issues. The try/except pattern is even more important here. Use the try/except pattern with a specific exception handler for FileNotFoundError and OSError (which covers permission and network errors).
🔗 Continue learning
Next in this series:
copy.deepcopy is necessary