File Paths with pathlib
If you’ve ever dealt with file paths in Python using string concatenation or the os.path module, you know it can get messy quickly. Backslashes on Windows, forward slashes on Linux, double slashes, missing separators — it’s easy to introduce bugs. The pathlib module solves these problems by giving you an object-oriented way to handle file paths.
Why pathlib Beats os.path
The old way involved functions like os.path.join(), os.path.exists(), os.path.splitext(), and so on. They work, but they feel clunky. You’re constantly passing strings around, and the code reads like a series of function calls rather than a coherent operation.
With pathlib, every path is an object with useful methods and attributes. You get tab completion in your editor, cleaner code, and fewer bugs. It’s been in the standard library since Python 3.4, so there’s no excuse not to use it.
from pathlib import Path
# Old way (os.path)
import os
path = os.path.join('folder', 'subfolder', 'file.txt')
# pathlib way
path = Path('folder') / 'subfolder' / 'file.txt'
See that / operator? It automatically handles the separator for you, so it works on any operating system.
Creating Paths
You can create a Path object from a string, or build it piece by piece:
from pathlib import Path
# From a string
p = Path('/home/user/projects')
# Build incrementally
p = Path('/home') / 'user' / 'projects'
# Current directory and home directory
cwd = Path.cwd()
home = Path.home()
# Create a path object from an existing string path
p = Path('/usr/local/bin/python')
You can also create paths from environment variables or other system information:
from pathlib import Path
import os
# From environment variable
config_path = Path(os.environ['HOME']) / '.config' / 'myapp'
# Relative paths
relative = Path('..') / 'sibling' / 'file.txt'
Reading and Writing Files
One of the nicest features of pathlib is that it directly exposes file operations. No need to open a separate file handle:
from pathlib import Path
# Write to a file
path = Path('example.txt')
path.write_text('Hello, world!')
# Read from a file
content = path.read_text()
print(content) # Hello, world!
# Binary mode works too
path.write_bytes(b'\x00\x01\x02')
data = path.read_bytes()
These one-liners are perfect for small files. For larger files or when you need more control, you can still use the traditional open() — pathlib works seamlessly with it:
path = Path('large_file.txt')
with open(path, 'r') as f:
for line in f:
print(line.strip())
Path Manipulation
Need to pull apart a path? Path objects give you intuitive attributes:
from pathlib import Path
path = Path('/home/user/documents/report.pdf')
print(path.name) # report.pdf
print(path.stem) # report
print(path.suffix) # .pdf
print(path.suffixes) # ['.pdf']
print(path.parent) # /home/user/documents
print(path.parent.parent) # /home/user
print(path.anchor) # /home/user/
print(path.parts) # ('/', 'home', 'user', 'documents', 'report.pdf')
You can also check file types and existence:
from pathlib import Path
path = Path('example.txt')
path.exists() # True if the path exists
path.is_file() # True if it's a regular file
path.is_dir() # True if it's a directory
path.is_symlink() # True if it's a symbolic link
path.is_absolute() # True if it's an absolute path
Renaming and moving files is straightforward:
from pathlib import Path
old = Path('old_name.txt')
new = Path('new_name.txt')
old.rename(new)
# Or use replace() for cross-platform atomic replacement
new.replace(Path('backup.txt'))
Glob Patterns
Finding files with glob patterns is incredibly clean:
from pathlib import Path
# All Python files in current directory
p = Path('.')
for py_file in p.glob('*.py'):
print(py_file)
# Recursively find all .txt files
for txt_file in p.glob('**/*.txt'):
print(txt_file)
# Find files with specific patterns
for config in p.glob('*.{json,yaml,yml}'):
print(config)
The ** pattern searches recursively through all subdirectories. This is perfect for tasks like finding all configuration files in a project.
Iterating Directories
If you need to walk through a directory tree, iterdir() is your friend:
from pathlib import Path
directory = Path('/home/user/projects')
# List all items in a directory
for item in directory.iterdir():
if item.is_file():
print(f'File: {item.name}')
elif item.is_dir():
print(f'Dir: {item.name}')
# Filter while iterating (Python 3.12+)
for item in directory.iterdir():
if item.suffix == '.py':
print(f'Python file: {item}')
Combine this with glob() or list comprehensions for powerful filtering:
# All image files in a directory and its subdirectories
images = list(Path('.').glob('**/*.{jpg,jpeg,png,gif}'))
# Directories only
dirs = [p for p in Path('.').iterdir() if p.is_dir()]
Creating Directories
Making directories is simple:
from pathlib import Path
# Create a single directory
Path('new_folder').mkdir()
# Create parent directories as needed
Path('deep/nested/folder').mkdir(parents=True)
# Don't error if it already exists
Path('existing_folder').mkdir(parents=True, exist_ok=True)
Path Resolution and Symbolic Links
Pathlib makes it easy to resolve symlinks and get absolute paths:
from pathlib import Path
path = Path('some/link')
# Resolve to absolute path, following symlinks
abs_path = path.resolve()
# Resolve but keep symlinks
real_path = path.resolve(strict=True)
# Make path absolute without resolving symlinks
abs_path = path.absolute()
Getting Started
The pathlib module should be your go-to for file path operations in Python. It works consistently across operating systems, the code is readable, and you get the benefits of IDE autocompletion and type checking.
Start replacing your os.path calls with pathlib.Path objects. You’ll write less code and have fewer bugs. The examples above cover most everyday tasks, and the module has even more features for specific use cases.
See Also
- os-module — Low-level operating system interfaces
- shutil-module — High-level file operations
- io-module — Core tools for working with streams