Skip to main content

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:

  1. Resolve and display the exact target.
  2. Validate that it is under the intended narrow root.
  3. Reject an empty path, filesystem root, home directory, or workspace root.
  4. Prefer a recoverable move or backup when practical.
  5. 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.

Source​

Explore connectionsOpen network