Blog / Coding tips

Python Check if File Exists: pathlib, os.path and Safe Patterns

Cover image for Python Check if File Exists: pathlib, os.path and Safe Patterns

To check if a file exists in Python, use Path("report.csv").is_file() from the built-in pathlib module. It returns True only when the path is an existing regular file, so a folder with the same name doesn't fool it. Use Path(...).is_dir() to check for a folder and Path(...).exists() when either will do. The older os.path.isfile(), os.path.isdir() and os.path.exists() functions give the same answers and are still fine. The bigger point, though, is that you often shouldn't check at all. If you're about to open, create or delete the file, it's safer to just do it and handle the exception, because the file can appear or disappear between your check and your next line of code.

This guide covers every version of the question I see: file versus folder, creating a file or directory only if it's missing, deleting a file only if it's there, checking before opening, matching a wildcard pattern, checking that a file isn't empty or is readable, relative paths that "exist" in one place but not another, and broken symbolic links. I ran every example on Python 3.13.5 on Linux on 7 October 2026, and the output under each block is pasted straight from the terminal. The same code works on Windows and macOS unless I say otherwise.

Check if a file or folder exists with pathlib

pathlib has been in the standard library since Python 3.4 and is the modern way to work with paths. You make a Path object once, then ask it questions. The script below creates a small demo folder with one CSV file, then asks three questions about three paths: a real file, a real folder and a missing file.

from pathlib import Path
import os

Path("demo").mkdir(exist_ok=True)
Path("demo/report.csv").write_text("name,score\nAmira,71\n")
Path("demo/empty.txt").write_text("")

for p in ["demo/report.csv", "demo", "demo/missing.csv"]:
    path = Path(p)
    print(f"{p:18} exists={path.exists()!s:5} is_file={path.is_file()!s:5} is_dir={path.is_dir()}")

print("os.path.exists:", os.path.exists("demo/report.csv"))
print("os.path.isfile:", os.path.isfile("demo"))
print("os.path.isdir: ", os.path.isdir("demo"))
demo/report.csv    exists=True  is_file=True  is_dir=False
demo               exists=True  is_file=False is_dir=True
demo/missing.csv   exists=False is_file=False is_dir=False
os.path.exists: True
os.path.isfile: False
os.path.isdir:  True

The table reads very clearly, and it shows the one thing beginners trip over most:

  • exists() is True for the file and for the folder. It only tells you that something is at that path.
  • is_file() is True only for the file. Use this when your next step is to open or read the file, because opening a folder fails.
  • is_dir() is True only for the folder. Use this before you list a directory or save something into it.
  • All three return False for a missing path. They never raise an exception for a path that isn't there, which makes them safe to call in an if.

The bottom three lines show the os.path versions. os.path.exists(), os.path.isfile() and os.path.isdir() take a string (or a Path) and return the same answers. There's no speed or correctness reason to switch existing code from one to the other. I use pathlib in new code because the same object can also build paths with /, read and write text, and list folders, so you write fewer imports and fewer string joins. If you're cleaning up a CSV once you've found it, my guide to reading a CSV in Python without pandas picks up from here.

Which one should you use?

  1. Reading or opening a file: is_file(), or better, just open it inside try (see the next section).
  2. Saving into a folder: mkdir(parents=True, exist_ok=True), which needs no check.
  3. Showing a message such as "Report already generated": is_file() is perfect, because nothing breaks if the answer goes stale.
  4. "Is anything at all using this name?": exists().

Check if a file exists in a specific folder

To check for a file inside a particular directory, join the folder and the file name with / rather than gluing strings together: (Path("demo") / "report.csv").is_file(). The / operator inserts the right separator for the operating system, so the same line works on Windows, macOS and Linux. If the folder name comes from a variable or user input, this also avoids the classic bug where a missing slash turns demo and report.csv into demoreport.csv, a path that will never exist. To look through a folder and every folder beneath it, use rglob(), which I show in the wildcard section further down.

Check before opening, or just open it?

The classic beginner pattern is "if the file exists, open it; otherwise use a default". Python developers call that look before you leap. The alternative, easier to ask forgiveness than permission (EAFP), is to open the file and catch the specific error. EAFP is usually better here, because the exception tells you why it failed, and there's no gap between the check and the open.

from pathlib import Path

def read_settings(name):
    try:
        return Path(name).read_text(encoding="utf-8")
    except FileNotFoundError:
        return "(defaults)"
    except IsADirectoryError:
        return "(that is a folder)"
    except PermissionError:
        return "(no permission)"

print(read_settings("demo/report.csv").splitlines()[0])
print(read_settings("demo/nope.ini"))
print(read_settings("demo"))
name,score
(defaults)
(that is a folder)

Three different problems get three different, honest messages. FileNotFoundError means nothing is there. IsADirectoryError means you pointed at a folder (on Windows this case raises PermissionError instead). PermissionError means the file exists but your program isn't allowed to read it. An if path.exists() check would have said "yes" for the folder and then crashed on the read, and it can't tell you about permissions at all.

Notice that I catch the specific errors. A bare except: or except Exception: would also hide a typo in the file name, a Unicode decoding error or a bug in your own code, and you'd see "(defaults)" without ever knowing something went wrong. Catching only what you expect is the most important habit in file handling.

Create a file or folder only if it doesn't exist

This is the most common reason people search for the check, and it's the case where the check is most often a mistake. You don't need it for folders, and for files there's a mode built for exactly this job.

from pathlib import Path

# Directory: create it if it does not exist (no check needed)
out = Path("demo/exports/2026")
out.mkdir(parents=True, exist_ok=True)
out.mkdir(parents=True, exist_ok=True)   # second call is fine
print("dir exists:", out.is_dir())

# File: create only if it does not exist, atomically
log = out / "log.txt"
for attempt in (1, 2):
    try:
        with open(log, "x", encoding="utf-8") as f:
            f.write("created\n")
        print(f"attempt {attempt}: created {log}")
    except FileExistsError:
        print(f"attempt {attempt}: {log} already exists, left alone")

# Append mode creates the file if missing, never truncates
with open(out / "notes.txt", "a", encoding="utf-8") as f:
    f.write("line\n")
print("notes lines:", len((out / "notes.txt").read_text().splitlines()))
dir exists: True
attempt 1: created demo/exports/2026/log.txt
attempt 2: demo/exports/2026/log.txt already exists, left alone
notes lines: 1

Here's what each part does:

  • mkdir(parents=True, exist_ok=True) creates demo/exports/2026 and any missing parents in one call. exist_ok=True means "don't complain if it's already there", so the second call is silently fine. This is the whole answer to "python check if directory exists and create if not": skip the check and call this.
  • open(path, "x") is exclusive creation. It creates the file and opens it for writing, but raises FileExistsError if the file is already there. The check and the creation happen in one step inside the operating system, so nothing can sneak in between them. On the first attempt the log is created; on the second it's left alone.
  • open(path, "a") is append mode. It creates the file if it's missing and adds to the end if it isn't, and it never wipes what's already there. If you just want "create it if needed, then add a line", append mode needs no check either.

Don't use "w" when you mean "create if missing". Write mode creates the file if it's missing, but it also empties an existing file the moment it opens it, before you've written anything. If you're saving structured data, the same rule applies to my guide on writing JSON to a file with pretty printing: decide whether you want to overwrite, and pick the mode on purpose.

Why checking first can go wrong (race conditions)

Checking first and then creating is called check-then-act, and it has a gap. The script below fakes two processes, A and B, that both want to create the same file. Both check first, both see that it's missing, and both write.

from pathlib import Path
p = Path("demo/race.txt")
p.unlink(missing_ok=True)
# Check-then-act: two "processes" both see the file missing
a_sees = p.exists(); b_sees = p.exists()
if not a_sees: p.write_text("from A\n")
if not b_sees: p.write_text("from B\n")
print("check-then-write result:", p.read_text().strip())
p.unlink()
for who in ("A", "B"):
    try:
        with open(p, "x") as f: f.write(f"from {who}\n")
    except FileExistsError:
        print(who, "got FileExistsError")
print("mode 'x' result:", p.read_text().strip())
check-then-write result: from B
B got FileExistsError
mode 'x' result: from A

With check-then-write, B silently overwrote A's file, and neither of them knew. With mode "x", A created the file and B got a clear FileExistsError, so A's data survived. In a real program the two "processes" might be two web requests, two scheduled jobs or two copies of a script someone double-clicked, and the gap is tiny but real. This is the same kind of bug that unique constraints solve in a database, which I cover in my post on preventing SQL injection with PDO prepared statements from the PHP side.

Delete a file only if it exists

The same idea works for deleting. Since Python 3.8, Path.unlink() takes missing_ok=True, which deletes the file if it's there and does nothing if it isn't.

from pathlib import Path

tmp = Path("demo/old.tmp")
tmp.write_text("x")
tmp.unlink(missing_ok=True)
tmp.unlink(missing_ok=True)       # no error the second time
print("old.tmp exists:", tmp.exists())

try:
    Path("demo/ghost.tmp").unlink()
except FileNotFoundError as e:
    print("without missing_ok:", type(e).__name__, "-", e.strerror)
old.tmp exists: False
without missing_ok: FileNotFoundError - No such file or directory

The first two calls show that deleting twice is safe. The last block shows what happens without missing_ok: Python raises FileNotFoundError. If you're stuck on an older Python, wrap the call in try/except FileNotFoundError: pass, which does the same job. unlink() only removes files and symbolic links. For an empty folder use Path.rmdir(), and for a folder with contents use shutil.rmtree(), with great care, because there's no recycle bin.

Check that a file isn't empty, is readable, or matches a pattern

Sometimes "exists" isn't enough. A half-finished export might leave a zero-byte file behind, or you may only know part of the file name.

Non-empty, readable and writable files

from pathlib import Path
import os

def has_content(p):
    p = Path(p)
    return p.is_file() and p.stat().st_size > 0

for name in ["demo/report.csv", "demo/empty.txt", "demo/missing.csv"]:
    print(f"{name:18} non-empty file: {has_content(name)}")

p = Path("demo/report.csv")
print("readable:", os.access(p, os.R_OK), "writable:", os.access(p, os.W_OK))
print("dir empty?", not any(Path("demo/exports/2026").iterdir()))
demo/report.csv    non-empty file: True
demo/empty.txt     non-empty file: False
demo/missing.csv   non-empty file: False
readable: True writable: True
dir empty? False

has_content() checks is_file() first, so stat() is never called on a missing file (which would raise FileNotFoundError). Then st_size > 0 rules out empty files. The and short-circuits, so the second part only runs when the first is true.

os.access(path, os.R_OK) asks whether the current user could read the file, and os.W_OK asks about writing. These are good for showing a helpful message, but the same race-condition warning applies: permissions can change, so still handle PermissionError when you actually open the file. The last line checks whether a folder is empty: any(folder.iterdir()) stops at the first entry it finds, so it's fast even for huge folders. It printed False here because the earlier script put two files in that folder.

Check with a wildcard

Path.glob() returns matching paths lazily, so wrap it in any() for a yes/no answer or sorted() to see the matches.

from pathlib import Path
import glob

Path("demo/invoice_2026-09.pdf").write_text("a")
Path("demo/invoice_2026-10.pdf").write_text("b")

matches = sorted(Path("demo").glob("invoice_*.pdf"))
print("any invoices?", bool(matches))
print([m.name for m in matches])
print("glob module:", sorted(glob.glob("demo/invoice_*.pdf")))
print("none match:", any(Path("demo").glob("receipt_*.pdf")))
any invoices? True
['invoice_2026-09.pdf', 'invoice_2026-10.pdf']
glob module: ['demo/invoice_2026-09.pdf', 'demo/invoice_2026-10.pdf']
none match: False

any(Path("demo").glob("receipt_*.pdf")) printed False because no receipts exist. glob patterns use * for any characters, ? for one character and ** for "any folders below here" (as in Path(".").glob("**/*.csv"), or the shorter Path(".").rglob("*.csv")). The glob module does the same with plain strings. I sort the results because the operating system can return them in any order, and an unsorted list makes tests flaky. If you need cleverer matching than wildcards, a regular expression over p.name works well. My post on Python f-strings shows a tidy way to build the file names you're searching for, such as f"invoice_{year}-{month:02d}.pdf".

Relative paths and symbolic links: why a file "doesn't exist" when it does

The most common complaint I get is "the file is right there but Python says it doesn't exist". Nine times out of ten it's a relative path.

Relative paths depend on the current working directory

from pathlib import Path
import os

print("cwd name:", Path.cwd().name)
here = Path(__file__).resolve().parent
print("script folder name:", here.name)
print("relative to cwd:", Path("demo/report.csv").exists())
os.chdir("/tmp")
print("after chdir, relative:", Path("demo/report.csv").exists())
print("anchored to script:", (here / "demo" / "report.csv").exists())
cwd name: post01
script folder name: post01
relative to cwd: True
after chdir, relative: False
anchored to script: True

A relative path like demo/report.csv is relative to the current working directory, which is wherever you launched Python from. It is not relative to the script's own folder. When I ran the script from its own folder, the file existed. After os.chdir("/tmp"), the same relative path pointed somewhere else and the check said False. The same thing happens when you run a script from a different folder in the terminal, from a cron job, from a scheduled task on Windows or from an IDE that sets its own working directory.

The fix is to anchor paths to the script itself: Path(__file__).resolve().parent / "demo" / "report.csv". That works no matter where the script is launched from. If a path still looks wrong, print Path(p).resolve() to see the full absolute path Python is actually checking. On Windows, also remember that backslashes in normal strings start escape sequences, so write r"C:\Users\me\file.txt", use forward slashes, or build the path with Path and /.

Broken symbolic links

A symbolic link (symlink) is a pointer to another path. If the target is deleted, the link is "broken", and the checks follow the link by default.

from pathlib import Path
import os

target = Path("demo/target.txt"); target.write_text("hi")
link = Path("demo/link.txt")
link.unlink(missing_ok=True); link.symlink_to("target.txt")
target.unlink()   # break the link
print("exists():", link.exists())
print("exists(follow_symlinks=False):", link.exists(follow_symlinks=False))
print("is_symlink():", link.is_symlink())
print("os.path.lexists:", os.path.lexists(link))
link.unlink()
exists(): False
exists(follow_symlinks=False): True
is_symlink(): True
os.path.lexists: True

exists() returned False because it followed the link to a target that's gone, even though the link itself is still sitting in the folder. Since Python 3.12, exists(follow_symlinks=False) checks the link itself, and is_symlink() and os.path.lexists() give the same answer on older versions. This matters when you're cleaning up: if you test exists() before deleting, a broken link is skipped and stays there forever. unlink(missing_ok=True) removes it either way.

Common mistakes when checking if a file exists

  1. Using exists() when you mean is_file(). A folder called data.csv passes exists() and then crashes open(). Pick the check that matches what you'll do next.
  2. Checking, then acting. if not p.exists(): p.write_text(...) can overwrite a file created in between. Use open(p, "x") to create, unlink(missing_ok=True) to delete and mkdir(exist_ok=True) for folders.
  3. Opening with "w" to "create if missing". Write mode empties existing files instantly. Use "x" or "a".
  4. Trusting a relative path. It depends on where Python was started. Anchor it to Path(__file__).resolve().parent, or print resolve() while debugging.
  5. Catching every exception. except Exception hides typos, encoding errors and bugs. Catch FileNotFoundError, IsADirectoryError and PermissionError by name.
  6. Forgetting case sensitivity. Report.csv and report.csv are different files on Linux, but the same file on default Windows and macOS set-ups. Code that "works on my laptop" can fail on a Linux server.
  7. Calling stat() on a path that might be missing. It raises FileNotFoundError. Check is_file() first or catch the error.
  8. Unescaped Windows paths. "C:\new\test.txt" contains a newline (\n) and a tab (\t). Use a raw string or forward slashes.

FAQ

How do I check if a file exists in Python without exceptions?

Call Path("name.txt").is_file() from pathlib, or os.path.isfile("name.txt"). Both return True or False and never raise an exception for a missing file. They're ideal for messages and if statements. If your next step is to open the file, it's still worth catching FileNotFoundError, because the file can be removed between the check and the open.

What is the difference between os.path.exists and os.path.isfile?

os.path.exists() returns True for anything at that path, including folders. os.path.isfile() returns True only for regular files (and symlinks pointing to files). In my test, both were True for demo/report.csv, but only exists() was True for the demo folder. pathlib has the same pair: exists() and is_file().

How do I create a folder in Python if it doesn't exist?

Use Path("exports/2026").mkdir(parents=True, exist_ok=True). It creates the folder and any missing parent folders, and does nothing if it's already there, so you don't need a separate check. The older equivalent is os.makedirs("exports/2026", exist_ok=True). Both raise FileExistsError only if a file is in the way with that name.

Why does Python say my file does not exist when it does?

Usually because the path is relative and Python is running from a different folder. Print Path("your/file.txt").resolve() to see the full path being checked. Anchor paths to the script with Path(__file__).resolve().parent / "file.txt". Also check spelling and capital letters (Linux is case-sensitive), hidden extensions such as report.csv.txt on Windows, and broken symbolic links.

Is pathlib better than os.path?

For new code, I'd say yes, because one Path object can join paths with /, check them, read and write text, and list folders, so you write less code. The answers are identical, though, and os.path isn't deprecated. Mixing them is fine too, because the os functions accept Path objects. The official pathlib documentation lists every method, including exists(), is_file() and the follow_symlinks option.

If you write files as well as checking for them, see my guide to writing a list to a CSV file in Python, which uses the same pathlib habits.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

PHP Sort Multidimensional Array by Value (usort, Tested)
Coding tips

PHP Sort Multidimensional Array by Value (usort, Tested)

usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.

October 7, 2026 · 17 min read · 0 views