Python pathlib: The Complete Guide to Modern File Paths
Replace os.path with pathlib for cleaner, more readable file path handling in Python. Covers all essential operations with examples.
What you'll learn
- ✓Why pathlib is better than os.path for most tasks
- ✓Creating, joining, and manipulating paths
- ✓Reading, writing, and searching files with pathlib
- ✓Practical recipes for common file operations
Prerequisites
- •Basic Python knowledge
- •Familiarity with file systems
Why pathlib Over os.path?
The os.path module treats paths as strings. This leads to verbose code with lots of os.path.join, os.path.dirname, and os.path.splitext calls. The pathlib module, introduced in Python 3.4, treats paths as objects with methods and operators. The result is cleaner, more readable code.
# os.path way
import os
config_path = os.path.join(os.path.dirname(__file__), 'config', 'settings.json')
name = os.path.splitext(os.path.basename(config_path))[0]
# pathlib way
from pathlib import Path
config_path = Path(__file__).parent / 'config' / 'settings.json'
name = config_path.stem
The pathlib version reads like English. The / operator joins paths naturally.
Creating Path Objects
from pathlib import Path
# From a string
p = Path("/usr/local/bin")
# Current directory
cwd = Path.cwd()
# Home directory
home = Path.home()
# Relative path
relative = Path("src/main.py")
# From components
composed = Path("usr", "local", "bin")
print(composed) # usr/local/bin
Path automatically uses the right path separator for your OS. On Windows it uses backslashes internally, on Unix it uses forward slashes.
Path Components
Every path can be decomposed into useful parts:
from pathlib import Path
p = Path("/home/user/projects/app/main.py")
print(p.name) # "main.py" - filename with extension
print(p.stem) # "main" - filename without extension
print(p.suffix) # ".py" - file extension
print(p.parent) # /home/user/projects/app
print(p.anchor) # "/" - root
print(p.parts) # ('/', 'home', 'user', 'projects', 'app', 'main.py')
# Multiple suffixes
archive = Path("data.tar.gz")
print(archive.suffixes) # ['.tar', '.gz']
print(archive.stem) # "data.tar"
Joining and Building Paths
The / operator is the primary way to join paths:
from pathlib import Path
base = Path("/home/user")
config = base / "config" / "app.toml"
print(config) # /home/user/config/app.toml
# Using joinpath for dynamic parts
parts = ["src", "utils", "helpers.py"]
full = base.joinpath(*parts)
print(full) # /home/user/src/utils/helpers.py
Modifying Paths
Pathlib provides methods to create new paths from existing ones:
from pathlib import Path
source = Path("reports/2026/q2/sales.csv")
# Change extension
json_path = source.with_suffix(".json")
print(json_path) # reports/2026/q2/sales.json
# Change filename
summary = source.with_name("summary.csv")
print(summary) # reports/2026/q2/summary.csv
# Change stem (keep extension)
backup = source.with_stem("sales_backup")
print(backup) # reports/2026/q2/sales_backup.csv
# Navigate up
parent = source.parent
print(parent) # reports/2026/q2
grandparent = source.parents[1]
print(grandparent) # reports/2026
Checking Path Properties
from pathlib import Path
p = Path("some/path")
p.exists() # Does the path exist?
p.is_file() # Is it a regular file?
p.is_dir() # Is it a directory?
p.is_symlink() # Is it a symbolic link?
p.is_absolute() # Is it an absolute path?
# Get file info
if p.exists():
stat = p.stat()
print(f"Size: {stat.st_size} bytes")
print(f"Modified: {stat.st_mtime}")
Reading and Writing Files
Pathlib has built-in methods for common file operations:
from pathlib import Path
# Writing text
config = Path("config.txt")
config.write_text("debug=true\nlog_level=info\n")
# Reading text
content = config.read_text()
print(content)
# Writing bytes
data = Path("output.bin")
data.write_bytes(b"\x00\x01\x02\x03")
# Reading bytes
raw = data.read_bytes()
# For larger files, use open()
log = Path("app.log")
with log.open("a") as f:
f.write("New log entry\n")
# Read lines
lines = Path("data.txt").read_text().splitlines()
Note that write_text and write_bytes overwrite the file entirely. For appending, use the open() method with mode "a".
Directory Operations
from pathlib import Path
# Create directories
Path("output/reports/2026").mkdir(parents=True, exist_ok=True)
# List directory contents
project = Path(".")
for item in project.iterdir():
kind = "DIR " if item.is_dir() else "FILE"
print(f"{kind}: {item.name}")
# Glob patterns
python_files = list(project.glob("*.py"))
print(f"Found {len(python_files)} Python files")
# Recursive glob
all_python = list(project.rglob("*.py"))
print(f"Found {len(all_python)} Python files (recursive)")
# Multiple patterns
from itertools import chain
sources = chain(project.rglob("*.py"), project.rglob("*.pyx"))
Practical Recipes
Finding and Processing Files
from pathlib import Path
import json
def process_json_files(directory):
"""Read all JSON files in a directory tree."""
results = {}
for json_file in Path(directory).rglob("*.json"):
data = json.loads(json_file.read_text())
results[json_file.stem] = data
return results
Safe File Writing with Temporary Files
from pathlib import Path
import tempfile
import shutil
def safe_write(target: Path, content: str):
"""Write to a temp file first, then rename for atomicity."""
target.parent.mkdir(parents=True, exist_ok=True)
tmp = Path(tempfile.mktemp(dir=target.parent, suffix=".tmp"))
try:
tmp.write_text(content)
tmp.replace(target) # Atomic on most filesystems
except Exception:
tmp.unlink(missing_ok=True)
raise
Building Output Paths from Input Paths
from pathlib import Path
def convert_paths(input_dir, output_dir, new_suffix):
"""Mirror input directory structure in output with new extension."""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
for source in input_dir.rglob("*"):
if source.is_file():
relative = source.relative_to(input_dir)
dest = output_dir / relative.with_suffix(new_suffix)
dest.parent.mkdir(parents=True, exist_ok=True)
print(f"{source} -> {dest}")
# convert_paths("data/raw", "data/processed", ".json")
Resolving and Normalizing Paths
from pathlib import Path
# Resolve to absolute path, resolving symlinks
absolute = Path("./relative/../path").resolve()
print(absolute)
# Expand user home directory
user_config = Path("~/config/app.toml").expanduser()
print(user_config)
# Get relative path
base = Path("/home/user/projects")
full = Path("/home/user/projects/app/src/main.py")
relative = full.relative_to(base)
print(relative) # app/src/main.py
Compatibility with os.path
Many standard library functions now accept Path objects. For those that do not, convert with str():
from pathlib import Path
import os
p = Path("/some/path")
# These all accept Path objects directly
os.chdir(p)
open(p)
# If a function requires a string
legacy_function(str(p))
# Convert os.path result to Path
old_style = os.path.expandvars("$HOME/config")
modern = Path(old_style)
Wrapping Up
pathlib makes file path handling in Python more readable, less error-prone, and more enjoyable. The object-oriented API with the / operator, built-in file I/O methods, and glob support covers the vast majority of file system tasks. Unless you are maintaining legacy code, prefer pathlib.Path over os.path in all new Python code.
Related articles
- Python Python Concurrency: asyncio vs threading vs multiprocessing
Compare Python's concurrency models side by side. Learn when to use asyncio, threading, or multiprocessing with practical benchmarks and real-world examples.
- Python Python Environments: venv, pyenv, and Poetry Guide
Master Python environment management with venv for virtual environments, pyenv for version switching, and Poetry for dependency management.
- Python Python Match Statement: Structural Pattern Matching
Master Python's match statement with structural pattern matching. Learn literal, sequence, mapping, class, guard, and OR patterns with practical examples.
- Python Pydantic v2: Data Validation and Settings Management
Learn Pydantic v2 for data validation, serialization, and settings management in Python. Covers models, validators, computed fields, and BaseSettings.