DEV Community

Kai Thorne
Kai Thorne

Posted on

Stop Using os.path — Python pathlib Makes File Handling Actually Enjoyable

If you're still writing os.path.join("dir", "subdir", "file.txt"), you're doing file paths the hard way.

Python 3.4 introduced pathlib — a modern object-oriented approach to filesystem paths. And since Python 3.6, it's been "the way" according to the standard library docs themselves. Yet I still see tutorials and production code alike clinging to the old string-based os.path functions.

Let me show you why pathlib is the upgrade you didn't know you needed.

The Core Idea: Paths Are Objects, Not Strings

The fundamental shift is simple: instead of passing strings around and hoping functions parse them correctly, you work with Path objects that have methods for everything.

from pathlib import Path

# Old way
import os.path
config_path = os.path.join(os.path.dirname(__file__), "config", "settings.yaml")

# pathlib way
config_path = Path(__file__).parent / "config" / "settings.yaml"
Enter fullscreen mode Exit fullscreen mode

The / operator works with paths because Path overrides it. No more os.path.join nesting. No more forgetting a separator.

What I Actually Use pathlib For Every Day

1. Traversing Directories

Need to find all markdown files in a project tree?

for md_file in Path("docs").rglob("*.md"):
    print(md_file.relative_to(Path.cwd()))
Enter fullscreen mode Exit fullscreen mode

.rglob("*pattern*") recursively matches. .glob("*pattern*") is non-recursive. Both return generators, so they're memory-friendly even on large trees.

Compare with os.walk + fnmatch:

# Old way
import os, fnmatch
for root, dirs, files in os.walk("docs"):
    for f in fnmatch.filter(files, "*.md"):
        print(os.path.relpath(os.path.join(root, f)))
Enter fullscreen mode Exit fullscreen mode

The pathlib version is 3x fewer lines and doesn't make you think about joining paths inside a loop.

2. Reading and Writing Files

This is where pathlib shines brightest:

# Read
data = Path("config.json").read_text()

# Write  
Path("output.txt").write_text("Hello, pathlib!")

# Binary
bytes_data = Path("image.png").read_bytes()
Path("copy.png").write_bytes(bytes_data)
Enter fullscreen mode Exit fullscreen mode

No with open(...) as f: for simple operations. No forgetting to close files. No encoding shenanigans with default system encoding — read_text() uses UTF-8 by default.

3. Checking File Properties

p = Path("some_file.py")

p.exists()          # Does it exist?
p.is_file()         # Is it a file?
p.is_dir()          # Is it a directory?
p.stat().st_size    # File size in bytes
p.stat().st_mtime   # Last modified timestamp
p.suffix            # '.py'
p.stem              # 'some_file' (name without suffix)
p.name              # 'some_file.py'
p.parent            # Path('.') — the containing directory
Enter fullscreen mode Exit fullscreen mode

All on the same object. No os.path.getsize(), os.path.isdir(), os.path.splitext() from five different import lines.

4. Creating and Deleting

# Create directory (like mkdir -p)
Path("data/2024/raw").mkdir(parents=True, exist_ok=True)

# Create a temp file
Path("/tmp/scratch.txt").touch()

# Delete
Path("old_backup.zip").unlink(missing_ok=True)  # Python 3.8+

# Recursive delete
import shutil
shutil.rmtree(Path("temp_dir"))
Enter fullscreen mode Exit fullscreen mode

The parents=True flag is the -p flag you always wanted. exist_ok=True means no crash if the directory already exists.

5. Working With Relative Paths

base = Path("/home/user/projects")
target = Path("/home/user/projects/src/utils/helpers.py")

# Relative path from base to target
rel = target.relative_to(base)  # Path('src/utils/helpers.py')

# Going back up
common = Path("/home/user/projects/src")
target.relative_to(common)       # Path('utils/helpers.py')
Enter fullscreen mode Exit fullscreen mode

Great for generating file listings, build scripts, or log messages that don't leak absolute filesystem structure.

The Pattern That Converted Me

Here's the exact refactor that made me a pathlib believer:

Before:

import os
import json

def load_config(env):
    base = os.path.dirname(os.path.abspath(__file__))
    config_dir = os.path.join(base, 'config')
    config_file = os.path.join(config_dir, f'{env}.json')

    if not os.path.exists(config_file):
        raise FileNotFoundError(f"No config for {env}")

    with open(config_file, 'r') as f:
        return json.load(f)
Enter fullscreen mode Exit fullscreen mode

After:

from pathlib import Path
import json

def load_config(env):
    config_file = Path(__file__).parent / 'config' / f'{env}.json'

    if not config_file.exists():
        raise FileNotFoundError(f"No config for {env}")

    return json.loads(config_file.read_text())
Enter fullscreen mode Exit fullscreen mode

Shorter. Cleaner. No import soup. No manual file handle management.

When You Still Need os.path

There are a few things os.path does that pathlib doesn't directly replace:

  • Low-level path splitting (os.path.splitdrive())
  • Some edge cases with UNC paths on Windows
  • Compatibility with code that strictly takes strings

But for 95% of everyday file operations, pathlib is the better choice. And if you need os.path functions, you can always get the string back with str(path_object).

Quick Reference

Task os.path way pathlib way
Join paths os.path.join(a, b) Path(a) / b
Get extension os.path.splitext(f)[1] Path(f).suffix
Check if file os.path.isfile(p) Path(p).is_file()
Read file open(p).read() Path(p).read_text()
Walk recursively os.walk() Path().rglob('*')
Get parent dir os.path.dirname(p) Path(p).parent
File name os.path.basename(p) Path(p).name
Absolute path os.path.abspath(p) Path(p).resolve()

Bottom Line

pathlib isn't just syntactic sugar — it changes how you think about file paths. Instead of assembling strings and passing them to helper functions, you ask a Path object to do the work. The result is code that's shorter, more readable, and harder to get wrong.

If you're on Python 3.6+, there's no reason not to use it. Your future self (and your code reviewers) will thank you.


Working with files is just one part of writing clean Python. For more Python patterns, check out the 100+ AI Coding Prompts for Developers — tested across Python, JavaScript, and TypeScript for debugging, refactoring, and shipping faster.

Top comments (0)