pathlib is the modern way to handle file paths in Python. It's cleaner and more intuitive than os.path.

Creating Paths

from pathlib import Path
 
# Current directory
cwd = Path.cwd()
 
# Home directory
home = Path.home()
 
# From string
path = Path("/usr/local/bin")
 
# Joining paths (use /)
config = Path.home() / ".config" / "myapp"

Path Components

path = Path("/home/user/documents/report.pdf")
 
path.name        # "report.pdf"
path.stem        # "report"
path.suffix      # ".pdf"
path.parent      # Path("/home/user/documents")
path.parents     # All ancestors
path.parts       # ('/', 'home', 'user', 'documents', 'report.pdf')
path.anchor      # "/"

Checking Paths

path = Path("myfile.txt")
 
path.exists()      # Does it exist?
path.is_file()     # Is it a file?
path.is_dir()      # Is it a directory?
path.is_symlink()  # Is it a symbolic link?
path.is_absolute() # Is it an absolute path?

Creating Files and Directories

# Create directory (and parents)
Path("data/processed").mkdir(parents=True, exist_ok=True)
 
# Create empty file (or update timestamp)
Path("output.txt").touch()

Reading and Writing

path = Path("data.txt")
 
# Write text
path.write_text("Hello, World!")
 
# Read text
content = path.read_text()
 
# Write bytes
path.write_bytes(b"\x00\x01\x02")
 
# Read bytes
data = path.read_bytes()
 
# With encoding
path.write_text("Héllo", encoding="utf-8")
content = path.read_text(encoding="utf-8")

Listing Directories

dir_path = Path("src")
 
# All items
for item in dir_path.iterdir():
    print(item)
 
# Pattern matching
for py_file in dir_path.glob("*.py"):
    print(py_file)
 
# Recursive
for py_file in dir_path.rglob("*.py"):
    print(py_file)
 
# As list
python_files = list(dir_path.glob("**/*.py"))

Path Operations

path = Path("src/module/file.py")
 
# Resolve to absolute
abs_path = path.resolve()
 
# Get relative path
rel_path = path.relative_to("src")  # Path("module/file.py")
 
# Change extension
new_path = path.with_suffix(".txt")  # src/module/file.txt
 
# Change name
new_path = path.with_name("other.py")  # src/module/other.py
 
# Change stem (keep extension)
new_path = path.with_stem("renamed")  # src/module/renamed.py

File Operations

from pathlib import Path
import shutil
 
src = Path("source.txt")
dst = Path("dest.txt")
 
# Rename/move
src.rename(dst)
 
# Copy (use shutil)
shutil.copy(src, dst)
shutil.copytree(Path("dir1"), Path("dir2"))
 
# Delete file
path.unlink()
 
# Delete empty directory
path.rmdir()
 
# Delete directory tree (use shutil)
shutil.rmtree(Path("dir_to_delete"))

File Metadata

path = Path("myfile.txt")
 
stat = path.stat()
stat.st_size      # Size in bytes
stat.st_mtime     # Modification time
stat.st_ctime     # Creation time (or metadata change on Unix)
 
# Convenient access
from datetime import datetime
modified = datetime.fromtimestamp(path.stat().st_mtime)

Working with Config Files

from pathlib import Path
import json
 
config_path = Path.home() / ".config" / "myapp" / "config.json"
 
# Ensure directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
 
# Write config
config_path.write_text(json.dumps({"theme": "dark"}))
 
# Read config
config = json.loads(config_path.read_text())

Temporary Files

from pathlib import Path
import tempfile
 
# Temp directory
with tempfile.TemporaryDirectory() as tmpdir:
    tmp_path = Path(tmpdir)
    (tmp_path / "data.txt").write_text("temporary")
# Directory deleted after block
 
# Temp file path
tmp_file = Path(tempfile.mktemp(suffix=".txt"))

Common Patterns

Safe file writing

def safe_write(path: Path, content: str):
    """Write atomically to avoid corruption."""
    tmp = path.with_suffix(".tmp")
    tmp.write_text(content)
    tmp.rename(path)

Find project root

def find_project_root(marker: str = "pyproject.toml") -> Path:
    """Find parent directory containing marker file."""
    current = Path.cwd()
    for parent in [current, *current.parents]:
        if (parent / marker).exists():
            return parent
    raise FileNotFoundError(f"No {marker} found")

Process all files

def process_directory(root: Path, pattern: str = "*.txt"):
    for file_path in root.rglob(pattern):
        content = file_path.read_text()
        # Process content
        file_path.write_text(content.upper())

os.path vs pathlib

# Old way (os.path)
import os
path = os.path.join(os.path.expanduser("~"), ".config", "app")
if os.path.exists(path) and os.path.isdir(path):
    files = os.listdir(path)
 
# New way (pathlib)
from pathlib import Path
path = Path.home() / ".config" / "app"
if path.is_dir():
    files = list(path.iterdir())

Quick Reference

from pathlib import Path
 
# Create
Path("dir").mkdir(parents=True, exist_ok=True)
Path("file.txt").touch()
 
# Read/Write
content = Path("f.txt").read_text()
Path("f.txt").write_text("data")
 
# Navigate
Path.cwd()          # Current directory
Path.home()         # Home directory
path / "child"      # Join paths
path.parent         # Parent directory
path.resolve()      # Absolute path
 
# Inspect
path.exists()
path.is_file()
path.is_dir()
path.suffix         # ".txt"
path.stem           # "file"
 
# List
path.iterdir()      # Direct children
path.glob("*.py")   # Pattern match
path.rglob("*.py")  # Recursive match

pathlib makes file operations readable and cross-platform. Use it instead of os.path.

React to this post: