Problem Solved

Python · check file exists

Home › Courses › Python › Problems › Check if a file exists
🐍 Concept · 3 min read

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.

check.py
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.

race.py
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.

patterns.py
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))