Skip to content
Codeloom
Python

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.

·5 min read · By Codeloom
Beginner 10 min read

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.