Python pathlib: The Modern Way to Handle File Paths
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
| Task | pathlib |
|---|---|
| Create path | Path("data/file.csv") |
| Join paths | Path("data") / "file.csv" |
| File name | path.name |
| File stem (no ext) | path.stem |
| Extension | path.suffix |
| Parent directory | path.parent |
| Check exists | path.exists() |
| Is file | path.is_file() |
| Is directory | path.is_dir() |
| Read text | path.read_text() |
| Write text | path.write_text("...") |
| Create directory | path.mkdir(parents=True, exist_ok=True) |
| List contents | path.iterdir() |
| Find files | path.glob("*.csv") |
| Find recursively | path.rglob("*.csv") |
| Delete file | path.unlink() |
| Rename/move | path.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.