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 POSIX

Directory 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 path

Stat 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))  # Writable
# 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 directory

Path 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: