Python pathlib: The Modern Way to Handle File Paths

· 5 min read

Introduction

For years, Python developers used os.path to work with file paths:

import os

path = os.path.join("data", "reports", "2026.csv")
filename = os.path.basename(path)
directory = os.path.dirname(path)
exists = os.path.exists(path)

This works, but it is verbose and not particularly readable. Python 3.4 introduced pathlib — an object-oriented approach to file paths that is cleaner, more intuitive, and now the recommended way to handle paths in modern Python.

All examples are tested on Python 3.12.


Creating Path Objects

from pathlib import Path

# Current directory
current = Path(".")

# Absolute path
home = Path.home()

# Specific path
config = Path("/etc/config")

# From string
data = Path("data/reports/2026.csv")

print(data)
print(type(data))

Expected output:

data/reports/2026.csv
<class 'pathlib.PosixPath'>

On Windows, you get WindowsPath instead of PosixPath — but the API is identical on all platforms. This is one of the key advantages over string-based paths.


Building Paths with /

The / operator joins path components — no more os.path.join():

from pathlib import Path

base = Path("data")
reports = base / "reports"
file = reports / "2026.csv"

print(file)

Expected output:

data/reports/2026.csv

You can chain as many components as needed:

path = Path("home") / "user" / "documents" / "project" / "main.py"
print(path)

Expected output:

home/user/documents/project/main.py

Path Properties

Path objects expose their components as properties:

from pathlib import Path

path = Path("data/reports/2026.csv")

print(path.name)        # 2026.csv
print(path.stem)        # 2026
print(path.suffix)      # .csv
print(path.suffixes)    # ['.csv']
print(path.parent)      # data/reports
print(path.parents[0])  # data/reports
print(path.parents[1])  # data
print(path.parts)       # ('data', 'reports', '2026.csv')

Expected output:

2026.csv
2026
.csv
['.csv']
data/reports
data/reports
data
('data', 'reports', '2026.csv')

Checking What Exists

from pathlib import Path

path = Path("data/reports/2026.csv")

print(path.exists())      # True or False
print(path.is_file())     # True if it's a file
print(path.is_dir())      # True if it's a directory
print(path.is_absolute()) # True if absolute path

Reading and Writing Files

pathlib has built-in methods for reading and writing — no need to open files manually for simple operations:

from pathlib import Path

path = Path("hello.txt")

# Write text
path.write_text("Hello, pathlib!\nSecond line.")

# Read text
content = path.read_text()
print(content)

# Write bytes
path.write_bytes(b"binary data")

# Read bytes
data = path.read_bytes()

Expected output:

Hello, pathlib!
Second line.

For more complex file operations (appending, reading line by line), use the standard open() with the path object:

with open(path, "a") as f:
    f.write("\nAppended line.")

Creating and Removing Directories

from pathlib import Path

# Create a directory
Path("output").mkdir(exist_ok=True)

# Create nested directories
Path("output/reports/2026").mkdir(parents=True, exist_ok=True)

# Remove an empty directory
Path("output/empty").rmdir()

# Remove a file
Path("output/old.txt").unlink(missing_ok=True)

exist_ok=True prevents errors if the directory already exists. parents=True creates all intermediate directories. missing_ok=True prevents errors if the file does not exist.


Finding Files with glob()

from pathlib import Path

# Find all Python files in the current directory
for f in Path(".").glob("*.py"):
    print(f)

# Recursive search — all .csv files in all subdirectories
for f in Path("data").rglob("*.csv"):
    print(f)

# All files matching a pattern
for f in Path(".").glob("report_*.txt"):
    print(f)

glob() searches the current directory only. rglob() searches recursively. Both return generators, so they are memory-efficient for large directory trees.


Listing Directory Contents

from pathlib import Path

# List everything in a directory
for item in Path(".").iterdir():
    print(item, "— dir" if item.is_dir() else "— file")

# List only files
files = [f for f in Path(".").iterdir() if f.is_file()]

# List only directories
dirs = [d for d in Path(".").iterdir() if d.is_dir()]

# Sort by name
sorted_files = sorted(Path(".").iterdir())

Renaming and Moving Files

from pathlib import Path

source = Path("old_name.txt")
source.write_text("some content")

# Rename in the same directory
source.rename("new_name.txt")

# Move to a different directory (also renames)
Path("new_name.txt").rename("output/new_name.txt")

# Copy a file (pathlib doesn't have copy — use shutil)
import shutil
shutil.copy(Path("source.txt"), Path("destination.txt"))

Replacing os.path

Here is a direct comparison of the old and new approaches:

import os
from pathlib import Path

# Old way
path = os.path.join("data", "file.csv")
name = os.path.basename(path)
ext = os.path.splitext(name)[1]
exists = os.path.exists(path)
parent = os.path.dirname(path)
os.makedirs("output/reports", exist_ok=True)

# New way
path = Path("data") / "file.csv"
name = path.name
ext = path.suffix
exists = path.exists()
parent = path.parent
Path("output/reports").mkdir(parents=True, exist_ok=True)

The pathlib version is shorter, more readable, and works identically on Windows, macOS, and Linux without any platform-specific separator handling.


A Real Example: Batch File Organizer

from pathlib import Path
import shutil

def organize_by_extension(source_dir: Path, target_dir: Path) -> None:
    source_dir = Path(source_dir)
    target_dir = Path(target_dir)

    for file in source_dir.iterdir():
        if not file.is_file():
            continue

        ext = file.suffix.lower().lstrip(".")
        if not ext:
            ext = "no_extension"

        dest_folder = target_dir / ext
        dest_folder.mkdir(parents=True, exist_ok=True)

        dest = dest_folder / file.name
        shutil.move(str(file), str(dest))
        print(f"Moved: {file.name}{ext}/")

organize_by_extension("downloads", "organized")

This script moves files from a downloads folder into subfolders named after their extension — pdf/, jpg/, mp3/, and so on. It pairs naturally with the file renaming guide for more complex batch operations.


Quick Reference

Taskpathlib
Create pathPath("data/file.csv")
Join pathsPath("data") / "file.csv"
File namepath.name
File stem (no ext)path.stem
Extensionpath.suffix
Parent directorypath.parent
Check existspath.exists()
Is filepath.is_file()
Is directorypath.is_dir()
Read textpath.read_text()
Write textpath.write_text("...")
Create directorypath.mkdir(parents=True, exist_ok=True)
List contentspath.iterdir()
Find filespath.glob("*.csv")
Find recursivelypath.rglob("*.csv")
Delete filepath.unlink()
Rename/movepath.rename(new_path)

Wrap-Up

pathlib replaces os.path with a cleaner, more Pythonic API. Path objects know their own components, handle cross-platform separators automatically, and come with built-in methods for the most common file operations.

For working with the files you find — reading CSVs, writing Excel files, or processing text — see the pandas guide and the openpyxl tutorial. For questions or future tutorial ideas, get in touch via the Contact page.