The pathlib module goes beyond basic path manipulation. Here's how to use it effectively.
Path Resolution and Normalization
from pathlib import Path
# Resolve symlinks and make absolute
path = Path("./relative/../path")
resolved = path.resolve() # Full absolute path
# Strict mode raises if path doesn't exist
try:
strict = path.resolve(strict=True)
except FileNotFoundError:
print("Path doesn't exist")
# Expand user home directory
home_path = Path("~/documents").expanduser()Pattern Matching
# Glob patterns
py_files = list(Path(".").glob("**/*.py")) # Recursive
configs = list(Path(".").glob("*.{json,yaml}")) # Won't work - use rglob
# Match method for individual paths
path = Path("data.json")
if path.match("*.json"):
print("JSON file")
# Case-insensitive matching (Python 3.12+)
# path.match("*.JSON", case_sensitive=False)Working with Parts
path = Path("/home/user/documents/report.pdf")
# Decompose paths
print(path.parts) # ('/', 'home', 'user', 'documents', 'report.pdf')
print(path.parent) # /home/user/documents
print(path.parents[0]) # /home/user/documents
print(path.parents[1]) # /home/user
print(path.anchor) # '/'
# Relative paths between locations
source = Path("/home/user/src")
target = Path("/home/user/docs/readme.md")
relative = target.relative_to(Path("/home/user")) # docs/readme.md
# Check if path is relative to another
target.is_relative_to(source) # False (Python 3.9+)File Operations
# Read/write with encoding
path = Path("data.txt")
content = path.read_text(encoding="utf-8")
path.write_text("new content", encoding="utf-8")
# Binary operations
data = path.read_bytes()
path.write_bytes(b"\x00\x01\x02")
# Atomic-ish writes using temp file
import tempfile
import shutil
def atomic_write(path: Path, content: str):
"""Write atomically by using temp file."""
path = Path(path)
with tempfile.NamedTemporaryFile(
mode='w',
dir=path.parent,
delete=False
) as tmp:
tmp.write(content)
temp_path = Path(tmp.name)
temp_path.replace(path) # Atomic on POSIXDirectory Operations
# Create nested directories
Path("a/b/c").mkdir(parents=True, exist_ok=True)
# Iterate with filtering
def python_files(directory: Path):
"""Yield Python files, skipping hidden directories."""
for item in directory.iterdir():
if item.name.startswith('.'):
continue
if item.is_dir():
yield from python_files(item)
elif item.suffix == '.py':
yield item
# Using walk (Python 3.12+)
# for root, dirs, files in Path(".").walk():
# dirs[:] = [d for d in dirs if not d.startswith('.')]
# for file in files:
# print(root / file)Path Manipulation
path = Path("/data/reports/2024/q1.csv")
# Change components
new_name = path.with_name("q2.csv") # /data/reports/2024/q2.csv
new_stem = path.with_stem("q2") # /data/reports/2024/q2.csv (3.9+)
new_suffix = path.with_suffix(".json") # /data/reports/2024/q1.json
new_parent = path.with_segments("/backup", "reports", "q1.csv") # 3.12+
# Multiple suffixes
archive = Path("data.tar.gz")
print(archive.suffixes) # ['.tar', '.gz']
print(archive.stem) # 'data.tar' (only removes last)
# Remove all suffixes
def remove_suffixes(path: Path) -> Path:
while path.suffix:
path = path.with_suffix('')
return pathStat and Metadata
path = Path("file.txt")
stat = path.stat()
# Common attributes
print(stat.st_size) # Size in bytes
print(stat.st_mtime) # Modification time (epoch)
print(stat.st_mode) # Permissions
# Convenience methods
print(path.owner()) # Username (Unix only)
print(path.group()) # Group name (Unix only)
# Permissions
import stat as stat_module
path.chmod(stat_module.S_IRUSR | stat_module.S_IWUSR) # rw-------
# Check access
import os
print(os.access(path, os.R_OK)) # Readable
print(os.access(path, os.W_OK)) # WritableSymlinks and Special Files
# Create symlinks
target = Path("original.txt")
link = Path("link.txt")
link.symlink_to(target)
# Check link status
print(link.is_symlink()) # True
print(link.resolve()) # Actual target path
# Read link target without resolving
print(link.readlink()) # Python 3.9+
# Stat without following symlinks
link.lstat()Cross-Platform Patterns
from pathlib import Path, PurePosixPath, PureWindowsPath
# Parse Windows paths on Unix (or vice versa)
win_path = PureWindowsPath(r"C:\Users\name\file.txt")
posix_path = PurePosixPath("/home/user/file.txt")
# Convert between styles
def to_posix_style(path: Path) -> str:
"""Convert any path to forward-slash style."""
return path.as_posix()
# Portable home directory
def get_config_dir() -> Path:
"""Get platform-appropriate config directory."""
if sys.platform == "win32":
return Path(os.environ.get("APPDATA", "~")).expanduser()
elif sys.platform == "darwin":
return Path("~/Library/Application Support").expanduser()
else:
return Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser()Context Manager Pattern
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path: Path):
"""Temporarily change working directory."""
original = Path.cwd()
try:
os.chdir(path)
yield path
finally:
os.chdir(original)
# Usage
with working_directory(Path("/tmp")):
# Operations happen in /tmp
for f in Path(".").iterdir():
print(f)
# Back to original directoryPath Comparison and Sorting
# Paths are comparable
paths = [Path("b.txt"), Path("a.txt"), Path("c.txt")]
sorted_paths = sorted(paths) # Lexicographic
# Same file check (resolves symlinks)
path1 = Path("file.txt")
path2 = Path("./file.txt")
print(path1.samefile(path2)) # True if same file
# Normalize for comparison
def normalize(path: Path) -> Path:
"""Normalize path for consistent comparison."""
return path.resolve()Integration with Standard Library
import json
import csv
from pathlib import Path
# JSON
data = json.loads(Path("config.json").read_text())
Path("output.json").write_text(json.dumps(data, indent=2))
# CSV
with Path("data.csv").open(newline='') as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
# Tempfile integration
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
(tmp_path / "file.txt").write_text("temp data")Common Recipes
def find_project_root(marker: str = "pyproject.toml") -> Path | None:
"""Find project root by looking for marker file."""
current = Path.cwd()
for parent in [current, *current.parents]:
if (parent / marker).exists():
return parent
return None
def ensure_dir(path: Path) -> Path:
"""Ensure directory exists, return path."""
path.mkdir(parents=True, exist_ok=True)
return path
def safe_name(name: str) -> str:
"""Make a filesystem-safe name."""
return "".join(c if c.isalnum() or c in ".-_" else "_" for c in name)
def backup_path(path: Path) -> Path:
"""Generate backup path with timestamp."""
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return path.with_name(f"{path.stem}_{timestamp}{path.suffix}")pathlib makes file system code readable and maintainable. Use it instead of os.path for new projects.
React to this post: