"How do I check whether a file exists?" is one of the highest-voted Python questions on Stack Overflow (over 7,300 upvotes), and the answer has changed since the question was first asked in 2009. In 2026 the canonical answer is pathlib.Path('path').exists(), with is_file() and is_dir() for type-specific checks. The older os.path.exists() still works, but pathlib reads cleaner and integrates with the rest of the path API. The trickier question (and the one that breaks production code) is WHEN to check at all: the Pythonic pattern is usually to skip the check and let the operation raise FileNotFoundError if needed, because the filesystem can change between your check and your action (the classic TOCTOU race). This entry covers all four checks, when to use each, the symlink edge cases, and the EAFP-vs-LBYL trade-off. Part of our Stdlib Essentials guide.
Key Takeaways
Path('p').exists() is the modern canonical check. It follows symlinks by default.
is_file(), is_dir(), is_symlink() answer more specific questions and return False if the path doesn't exist.
Don't check before opening. Use EAFP: try the open and catch FileNotFoundError. Avoids the TOCTOU race.
Check only when you won't act: deciding whether to create, skip, or report. For action, just act.
Python 3.12+ added follow_symlinks=False to exists(), is_file(), and is_dir() for explicit symlink semantics.
What each pathlib check returns for five common scenarios. Below: when to skip checking and let the action raise instead.
The Canonical 2026 Answer
For a simple "does this path exist" question, the modern answer is one line:
from pathlib import Path
if Path("/etc/hosts").exists():
print("the file is there")
That's the whole story for the basic case. exists() returns True if anything is at that path (file, directory, symlink to either), False otherwise. The behavior is the same on Linux, macOS, and Windows; pathlib normalizes the path separators internally.
For type-specific questions, use the matching predicate:
p = Path("/some/path")
p.exists() # anything at all?
p.is_file() # exists AND is a regular file?
p.is_dir() # exists AND is a directory?
p.is_symlink() # is THIS path a symlink?
The first three (exists, is_file, is_dir) follow symlinks by default. The last (is_symlink) does not; it tests whether the path itself is a symbolic link, regardless of what the link points to.
The Five Important Scenarios
Five distinct filesystem situations exercise these methods differently. The decision matrix above shows the full table; here's the prose version.
1. A regular file exists at the path
exists() returns True, is_file() returns True. is_dir() returns False. is_symlink() returns False. The common case.
2. A directory exists at the path
exists() returns True, is_dir() returns True. is_file() returns False (it's a directory, not a file). is_symlink() returns False.
3. A symlink points to a real file
This is the case that surprises developers new to symlinks. exists() and is_file() both return True because they follow the link to its target, which is a real file. is_dir() returns False. is_symlink() returns True because the path itself is a symlink.
4. A broken symlink (target was deleted)
The interesting failure mode. is_symlink() returns True because the link still exists. But exists() and is_file() both return False because they follow the link to a nonexistent target. is_dir() is also False. To detect a broken symlink specifically:
p = Path("link")
broken = p.is_symlink() and not p.exists()
This idiom is the canonical broken-link test.
5. Nothing at the path
All four return False. The path neither exists nor points to anything.
EAFP vs LBYL: When to Skip the Check
The most important lesson in this article is that you usually shouldn't check. The Pythonic pattern is EAFP: "Easier to Ask Forgiveness than Permission." Just try the operation and handle the exception if it fails.
# LBYL (Look Before You Leap) - usually wrong:
if Path("config.json").is_file():
with open("config.json") as f:
data = json.load(f)
else:
data = default_config()
# EAFP (Easier to Ask Forgiveness) - usually right:
try:
with open("config.json") as f:
data = json.load(f)
except FileNotFoundError:
data = default_config()
The EAFP version is shorter, idiomatic, and (critically) immune to the TOCTOU race. The LBYL version has a bug: between the is_file() call and the open(), the file could be deleted by another process, causing the open() to raise FileNotFoundError that the else branch doesn't catch.
The question to ask yourself: am I about to act on this path? If yes, skip the check. If no (you're displaying status, deciding whether to create something, or planning future work), the check is fine.
The TOCTOU Race Condition
TOCTOU stands for Time Of Check to Time Of Use. It's a class of bug where you check a filesystem state at one moment and act on it later, but the state changes between the two moments.
p = Path("important.log")
# Check
if p.exists():
# ANOTHER PROCESS DELETES THE FILE HERE
# Use
contents = p.read_text() # FileNotFoundError!
The window between check and use is small (microseconds), but on a busy server it gets hit. Logs rotate. Caches get cleaned. Temp directories get reaped. Whenever you write "check then act", you're betting nothing changes in between. EAFP doesn't take that bet.
TOCTOU is also a security concern. A privileged process that checks "is this file owned by the right user" and then opens it can be exploited by an attacker who swaps the file for a symlink to /etc/shadow in the race window. For security-sensitive operations, use os.open with O_NOFOLLOW or other atomic primitives.
os.path: The Legacy Equivalents
Before pathlib (added in Python 3.4 via PEP 428), the standard approach used os.path. The functions still work and are appropriate for code that needs to support older Python.
import os.path
os.path.exists(path) # equivalent to Path(path).exists()
os.path.isfile(path) # equivalent to Path(path).is_file()
os.path.isdir(path) # equivalent to Path(path).is_dir()
os.path.islink(path) # equivalent to Path(path).is_symlink()
For new code in 2026 targeting Python 3.10+, default to pathlib. The methods chain naturally with the rest of the Path API:
# pathlib chains:
config = Path.home() / ".myapp" / "config.json"
if config.exists():
data = json.loads(config.read_text())
# os.path scattered procedural:
config = os.path.join(os.path.expanduser("~"), ".myapp", "config.json")
if os.path.exists(config):
with open(config) as f:
data = json.load(f)
For mixed codebases, stay consistent within a module. Migrating piecewise leads to confusing reads where half the file uses pathlib and half uses os.path.
Python 3.12+: follow_symlinks Parameter
Python 3.12 added follow_symlinks=False to exists(), is_file(), and is_dir(). This lets you ask "does this path itself exist as a real entity" without following links.
# Symlink "link" points to deleted file
p = Path("link")
p.exists() # False (follows symlink to missing target)
p.exists(follow_symlinks=False) # True (the symlink itself exists)
For most code, the default behavior (follow symlinks) is what you want; a symlink that resolves to a real file should look like a real file. Use follow_symlinks=False when you specifically care about the link itself: filesystem indexers, backup tools, security audits.
No check; just try and handle the specific exceptions.
Create directory if it doesn't exist
One line, atomic, race-free:
Path("logs").mkdir(parents=True, exist_ok=True)
The exist_ok=True flag makes mkdir tolerate the case where the directory already exists. Covered fully in the mkdir -p guide.
Report missing inputs to the user
When you genuinely won't act, LBYL is fine:
def validate_inputs(paths: list[Path]) -> None:
missing = [p for p in paths if not p.exists()]
if missing:
raise ValueError(f"missing inputs: {', '.join(map(str, missing))}")
You're producing a user-facing error message, not opening the files; the check is appropriate.
The race window exists (another process could write output after your check), but the cost is just doing the work twice; the file ends up the same. Acceptable trade-off for many workflows.
Checking Multiple Files: glob and rglob
"Does ANY file matching this pattern exist?" is a related question that exists() doesn't answer directly. Use glob() for single-directory matches and rglob() for recursive matches:
from pathlib import Path
# Any .log file in the directory?
has_logs = any(Path("logs").glob("*.log"))
# Any .py file anywhere under the project?
has_python = any(Path("project").rglob("*.py"))
# All files matching the pattern (sorted)
sorted(Path("data").glob("snapshot-*.json"))
glob returns a generator. Wrapping it in any() short-circuits at the first hit, so the check stops as soon as a match is found rather than enumerating the whole directory. For "every file with this extension" patterns, this is the canonical idiom.
rglob walks the directory tree recursively. Be careful on huge filesystems (deeply nested projects, mounted network drives) where the walk can be slow; cap depth with manual os.walk if needed.
Permission Checks vs Existence Checks
A subtle case: exists() returns False if you don't have permission to access the parent directory, even when the file is genuinely there. The check confuses "absent" with "unreadable" and you can't tell which:
p = Path("/root/secret.txt")
print(p.exists()) # False even if the file exists, because /root is unreadable
For most application code this doesn't matter; you can't open the file anyway, so treating it as absent is fine. For security audits, system management, or anything that needs to distinguish "no file" from "permission denied", catch the specific exceptions:
try:
contents = Path("/root/secret.txt").read_text()
except FileNotFoundError:
print("file genuinely missing")
except PermissionError:
print("file may exist but cannot access")
This is another reason to default to EAFP: the exceptions carry more information than the boolean returned by exists().
Common Mistakes
1. Checking before opening
The canonical LBYL anti-pattern. Replace with EAFP and a try/except around the open.
2. Using exists() to test for a directory
# Wrong: passes even if "logs" is a file
if Path("logs").exists():
for entry in Path("logs").iterdir():
...
# Right: check the type
if Path("logs").is_dir():
for entry in Path("logs").iterdir():
...
iterdir() on a non-directory raises NotADirectoryError. Use is_dir() when you specifically need a directory.
3. Forgetting that exists() follows symlinks
If your code processes symlinks deliberately (a backup tool, a deduplication script), exists() can confuse you because it looks past the link. Use follow_symlinks=False on 3.12+ or check is_symlink() first.
4. Race conditions in concurrent code
If multiple threads or processes touch the same files, "check then act" is broken. Use file locks, atomic renames, or EAFP. The CPython issue tracker has examples of TOCTOU bugs in pathlib's own internals that were fixed in 2026 releases.
5. Using try/except for control flow on non-FileNotFoundError
EAFP is for "file might not exist". For "I don't have permission" or "this is a directory not a file", catch the specific exception or check the path's type first. Catching bare OSError swallows too much.
Frequently Asked Questions
What is the best way to check if a file exists in Python?
Use pathlib.Path('path/to/file').exists() for a simple yes/no check. For more specific questions, use is_file() (returns True only if the path exists AND is a regular file), is_dir() (True only if it exists AND is a directory), or is_symlink() (True if the path itself is a symlink, regardless of target). Path.exists() follows symlinks by default; use exists(follow_symlinks=False) (Python 3.12+) to check the link itself. The legacy os.path.exists() and os.path.isfile() still work and are appropriate for code that supports Python 3.5 or older.
Should I check if a file exists before opening it in Python?
Usually not. The Pythonic pattern is EAFP (Easier to Ask Forgiveness than Permission): just try to open the file and handle FileNotFoundError if it's missing. Checking first creates a TOCTOU race condition (Time Of Check to Time Of Use) where the file could be deleted between the check and the open, breaking your code in production. Use Path.exists() only when you genuinely don't intend to open the file: deciding whether to create something, deciding whether to skip work, or producing user-facing messages. For "I'm about to open this", skip the check and handle the exception.
What is the difference between Path.exists() and os.path.exists() in Python?
Functionally they answer the same question, but pathlib.Path.exists() is the modern object-oriented form (PEP 428, Python 3.4+) while os.path.exists() is the procedural legacy form. pathlib chains naturally with other Path operations (parent, with_suffix, joinpath) and reads as cleaner code: Path('config') / 'settings.json' beats os.path.join('config', 'settings.json'). For new code in 2026, default to pathlib. For maintaining older codebases that mix os.path everywhere, stay consistent with what's already there until you can migrate the whole module.
What is the TOCTOU race condition in Python file checks?
TOCTOU stands for Time Of Check to Time Of Use. It's a class of bug where you check a file's state (exists, is a file, has certain permissions) and then act on that state, but the filesystem changes between the check and the action. In Python, the classic pattern is: if Path(path).exists(): open(path). If another process deletes the file between exists() and open(), your code raises FileNotFoundError despite the check passing. The fix is to skip the check and use EAFP: try the operation directly and handle the exception. TOCTOU is also a security issue when the check is for permissions or ownership; an attacker can swap a file between check and use to bypass restrictions.
How do I check if a symbolic link exists in Python?
Use Path('link').is_symlink() to check whether the path itself is a symlink, regardless of whether the target exists. Path.exists() and Path.is_file() follow symlinks by default, so a broken symlink (one whose target was deleted) returns False from exists() even though the link is real. In Python 3.12+, pass follow_symlinks=False to exists() and is_file() to check the link without following it. For broken-symlink detection specifically, the idiom is Path('link').is_symlink() and not Path('link').exists(), which is True only when the link itself exists but its target doesn't.
The Bottom Line: Default to EAFP, Check Only When You Won't Act
Path('p').exists() is the canonical 2026 check; is_file(), is_dir(), and is_symlink() answer more specific questions. But the bigger lesson is when to skip the check: if you're about to act on the path, use EAFP and catch the exception. Checking creates a TOCTOU race that production code eventually hits. Reserve the existence checks for cases where you genuinely won't open the file: branching decisions, user-facing messages, planning. Next up: Python's mkdir -p equivalent. Or browse the full Stdlib Essentials guide.
Lock In Filesystem Reflexes
CodeGym's Python track turns pathlib patterns into reflex across 62 levels: existence checks, EAFP file opens, atomic directory creation, race-free workflows. AI validators catch the LBYL anti-pattern and TOCTOU bugs; AI mentors explain why a particular check is unnecessary and what to replace it with. First level free; full plan on the pricing page.
GO TO FULL VERSION