Filesystem and Modern Path Manipulation
Filesystem and Modern Path Manipulation
After reading this chapter, you will master object-oriented filesystem operations with pathlib.Path, perform recursive directory walking and globbing, stream large files safely in buffered chunks, manage high-level file tree copies and removals with shutil, and allocate secure ephemeral storage with tempfile.
Mental model
Legacy Python code used strings and functions from the os.path module (os.path.join, os.path.exists). In modern Python, the pathlib module models paths as rich objects with operator overloading:
Legacy String Manipulation:
path = os.path.join(os.path.join(base_dir, "logs"), "app.log")
Modern Object-Oriented Pathlib:
path = Path(base_dir) / "logs" / "app.log"
│
├── path.name ──▶ "app.log"
├── path.stem ──▶ "app"
├── path.suffix ──▶ ".log"
├── path.parent ──▶ Path(base_dir) / "logs"
└── path.exists() ──▶ True / False
The / operator is overloaded to perform cross-platform path concatenation (/ on Linux/macOS, \ on Windows) automatically.
Minimal example
Save as pathlib_showcase.py:
# pathlib_showcase.py
import tempfile
from pathlib import Path
def main() -> None:
# Use temporary directory for safe filesystem demonstrations
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
log_dir = root / "var" / "log" / "cluster"
# 1. Create nested directories (mkdir with parents=True)
log_dir.mkdir(parents=True, exist_ok=True)
print(f"Created directory tree: {log_dir}")
# 2. Write and read text directly without manual open/close
app_log = log_dir / "app.log"
app_log.write_text("2026-09-07T10:00:00Z [INFO] System initialized\n")
# Append more content
with app_log.open("a") as f:
f.write("2026-09-07T10:00:05Z [WARN] Memory watermark high\n")
# 3. Read back text
content = app_log.read_text()
print(f"\nFile content ({app_log.stat().st_size} bytes):")
print(content.strip())
# 4. Globbing for matching files
print(f"\nMatching files (*.log):")
for match in root.rglob("*.log"):
print(f" Found: {match.relative_to(root)}")
if __name__ == "__main__":
main()Run via uv run python pathlib_showcase.py:
Created directory tree: .../var/log/cluster
File content (87 bytes):
2026-09-07T10:00:00Z [INFO] System initialized
2026-09-07T10:00:05Z [WARN] Memory watermark high
Matching files (*.log):
Found: var/log/cluster/app.log
Worked examples
Case 1: Buffered Chunk Streaming for Gigabyte-Scale Files
Reading huge files with .read() loads the entire file into RAM, crashing systems with MemoryError. Reading in buffered chunks maintains \(O(1)\) memory consumption:
# stream_hasher.py
import hashlib
import tempfile
from pathlib import Path
def compute_large_file_sha256(file_path: Path, chunk_size: int = 65536) -> str:
hasher = hashlib.sha256()
with file_path.open("rb") as f:
# iter(lambda: f.read(chunk_size), b"") reads until empty bytes EOF sentinel
for chunk in iter(lambda: f.read(chunk_size), b""):
hasher.update(chunk)
return hasher.hexdigest()
if __name__ == "__main__":
with tempfile.NamedTemporaryFile() as tmp:
# Write 5 MB of dummy data
tmp.write(b"DATA_CHUNK" * (500_000))
tmp.flush()
digest = compute_large_file_sha256(Path(tmp.name))
print(f"Calculated SHA256 in chunks: {digest}")Run:
uv run python stream_hasher.pyOutput:
Calculated SHA256 in chunks: f11e74031d279cae560f7e15bf28c50424564c7811ef140f04746f34fc3d18e8
Case 2: Atomic File Writes via Temporary Renaming
Writing directly to an active file risks leaving corrupt, partial files if the process is terminated mid-write. Standard practice writes to a sibling temporary file and performs an atomic replace:
# atomic_writer.py
import os
import tempfile
from pathlib import Path
def write_file_atomically(target_path: Path, content: str) -> None:
# 1. Create temporary file in the same directory (guarantees same filesystem)
target_dir = target_path.parent
target_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", dir=target_dir, delete=False) as tmp:
tmp.write(content)
tmp.flush()
os.fsync(tmp.fileno()) # Flush OS buffers to physical disk
temp_path = Path(tmp.name)
# 2. Atomic rename replaces target instantly (POSIX atomic guarantee)
temp_path.replace(target_path)
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as tmpdir:
config_file = Path(tmpdir) / "app_config.json"
write_file_atomically(config_file, '{"version": 1, "active": true}')
print(f"File atomically created: {config_file.name}")
print(f"Content: {config_file.read_text()}")Run:
uv run python atomic_writer.pyOutput:
File atomically created: app_config.json
Content: {"version": 1, "active": true}
Pitfalls
Pitfall 1: Mixing os.path String Manipulation with Path Objects
Passing strings to functions expecting Path or concatenating str(path) + "/file" breaks cross-platform compatibility. Always stick to Path objects and the / operator.
Pitfall 2: Forgetting parents=True on mkdir()
Calling path.mkdir() fails with FileNotFoundError if intermediate parent directories do not already exist. Always supply parents=True, exist_ok=True when creating nested directory hierarchies.
Exercises
- Write a script that scans a directory and prints all files larger than 10 MB, displaying their sizes in megabytes.
- Implement a backup utility using
shutil.copy2that copies all.conffiles from a source directory into an archive directory while preserving original timestamps. - Write a function that counts the total number of lines across all
.pyfiles in a repository usingPath.rglob(). - Demonstrate how
pathlib.Path.resolve()canonicalizes symlinks and relative path navigation (..).
Further reading
- Python Standard Library:
pathlib,shutil,tempfile. - PEP 428: The pathlib Module — Object-oriented filesystem paths.