File Paths and Filesystem Operations
A path is a location description, not proof that a file exists. Prefer
pathlib.Path for ordinary application code because it keeps path construction
and filesystem operations in one cross-platform interface.
from pathlib import Path
root = Path("data")
report = root / "reports" / "summary.csv"
if report.is_file():
size = report.stat().st_size
Relative paths are interpreted against the process's current working directory, which is chosen by the caller and may differ from the script's directory. Make that base an explicit configuration or argument.
For a resource intentionally located beside a standalone script:
script_dir = Path(__file__).resolve().parent
template = script_dir / "templates" / "report.txt"
For installed package data, use importlib.resources; a package may not exist as
ordinary adjacent files.
Expansion and resolution
configured = Path("~/exports").expanduser()
absolute = configured.resolve()
expanduser() handles the home marker. resolve() makes a path absolute,
normalizes it, and resolves symbolic links. These operations have different
semantics and should not be treated as interchangeable string cleanup.
Do not use string-prefix checks to prove containment: /safe-backup begins with
/safe but is not inside it. Resolve the intended root and target, then use path
relationships such as relative_to. Even that check is only one part of a
security boundary because links and filesystem state can change between checking
and using a path.
Discovery
markdown_files = sorted(root.rglob("*.md"))
Glob result order is not a portable contract; sort when deterministic processing matters. Directory traversal can encounter permission failures, broken links, cycles introduced by link-following, and files that disappear during the scan.
Operations
Path.mkdir, rename, replace, unlink, and rmdir cover common single-path
operations. Use shutil.copy2, copytree, move, and rmtree for copying,
moving, or removing trees. Their metadata and cross-filesystem behavior differ,
so consult the operation's contract instead of assuming a rename or copy is
atomic.
Before deleting or overwriting:
- Resolve and display the exact target.
- Validate that it is under the intended narrow root.
- Reject an empty path, filesystem root, home directory, or workspace root.
- Prefer a recoverable move or backup when practical.
- Expect time-of-check/time-of-use races when other processes can change it.
An existence check followed by an operation is not a guarantee. Prefer the
operation itself and handle its specific exception. Avoid except OSError unless
all of its causes truly share the same recovery policy.
Path construction is not validation
The pathlib contract
allows an absolute right operand to replace the earlier path. Path("") means
the current directory, so reject an empty raw input before constructing a Path.
Path uses the host operating system's path rules; use PureWindowsPath or
PurePosixPath to inspect another platform's syntax without accessing its filesystem.
from pathlib import Path, PurePosixPath
from tempfile import TemporaryDirectory
assert PurePosixPath("/safe") / "/other" == PurePosixPath("/other")
assert Path("") == Path(".")
with TemporaryDirectory() as directory:
root = Path(directory)
reports = root / "reports"
reports.mkdir(parents=True, exist_ok=True)
draft = reports / "draft.txt"
draft.write_text("ready", encoding="utf-8")
final = draft.replace(reports / "final.txt")
assert final.name == "final.txt"
assert final.stem == "final" and final.suffix == ".txt"
assert final.read_text(encoding="utf-8") == "ready"
final.unlink()
reports.rmdir()
resolve() defaults to strict=False: it resolves as far as possible and can
return a path whose remaining components do not exist. Use strict=True when
missing components must raise FileNotFoundError, without mistaking this for a
lasting existence guarantee. __file__ is unavailable in some interactive contexts.
mkdir(exist_ok=True) still fails if the target is a non-directory.
rmdir() requires an empty directory; unlink() removes a file or a symbolic
link itself, not the link's target. replace() can overwrite an existing file;
rename() has platform-dependent behavior when the target already exists.
The example confines those operations to its own temporary directory.