Beyond basic path manipulation, pathlib enables elegant patterns for file operations.

Path Construction

from pathlib import Path
 
# Build paths naturally
base = Path('/home/user')
config = base / 'config' / 'app.yml'
 
# From components
Path('/', 'home', 'user', 'file.txt')
 
# Current directory
Path.cwd()
Path('.')
 
# Home directory
Path.home()
Path('~').expanduser()

File Operations

from pathlib import Path
 
path = Path('data.txt')
 
# Read/write (no open() needed)
content = path.read_text()
path.write_text('new content')
 
# Binary
data = path.read_bytes()
path.write_bytes(b'\x00\x01\x02')
 
# Touch (create if missing)
path.touch()
path.touch(exist_ok=False)  # Raises if exists

Directory Traversal

from pathlib import Path
 
directory = Path('project')
 
# Iterate immediate children
for child in directory.iterdir():
    print(child)
 
# Recursive glob
for py_file in directory.rglob('*.py'):
    print(py_file)
 
# Non-recursive glob
for txt in directory.glob('*.txt'):
    print(txt)
 
# Pattern matching
for test in directory.glob('**/test_*.py'):
    print(test)

Glob Patterns

from pathlib import Path
 
d = Path('.')
 
d.glob('*.py')           # Python files in current dir
d.glob('**/*.py')        # Python files recursively
d.glob('data_[0-9].csv') # data_0.csv through data_9.csv
d.glob('**/[!_]*')       # Files not starting with _
d.glob('**/*.{py,txt}')  # Won't work - use multiple globs

Path Properties

from pathlib import Path
 
p = Path('/home/user/docs/report.tar.gz')
 
p.name        # 'report.tar.gz'
p.stem        # 'report.tar'
p.suffix      # '.gz'
p.suffixes    # ['.tar', '.gz']
p.parent      # PosixPath('/home/user/docs')
p.parents[0]  # Same as parent
p.parents[1]  # PosixPath('/home/user')
p.parts       # ('/', 'home', 'user', 'docs', 'report.tar.gz')
p.root        # '/'
p.anchor      # '/'

Path Modification

from pathlib import Path
 
p = Path('/home/user/file.txt')
 
# Change extension
p.with_suffix('.md')      # /home/user/file.md
 
# Change name
p.with_name('other.txt')  # /home/user/other.txt
 
# Change stem (keep extension)
p.with_stem('new')        # /home/user/new.txt (Python 3.9+)
 
# Add to stem
p.parent / (p.stem + '_backup' + p.suffix)

Checking Paths

from pathlib import Path
 
p = Path('some/path')
 
p.exists()      # Path exists?
p.is_file()     # Is a file?
p.is_dir()      # Is a directory?
p.is_symlink()  # Is a symlink?
p.is_absolute() # Absolute path?
p.is_relative_to('/home')  # Under /home? (Python 3.9+)

Path Resolution

from pathlib import Path
 
p = Path('./scripts/../data/./file.txt')
 
# Resolve symlinks and normalize
p.resolve()  # /absolute/path/data/file.txt
 
# Resolve without requiring existence
p.resolve(strict=False)
 
# Relative to another path
p.relative_to('/absolute/path')  # data/file.txt
 
# Or use absolute()
p.absolute()  # Doesn't resolve symlinks

Directory Operations

from pathlib import Path
 
d = Path('new_directory')
 
# Create directory
d.mkdir()
d.mkdir(parents=True)          # Create parents too
d.mkdir(exist_ok=True)         # Don't error if exists
d.mkdir(parents=True, exist_ok=True)  # Safe creation
 
# Remove directory (must be empty)
d.rmdir()
 
# Remove file
Path('file.txt').unlink()
Path('file.txt').unlink(missing_ok=True)  # Python 3.8+

File Metadata

from pathlib import Path
from datetime import datetime
 
p = Path('file.txt')
 
stat = p.stat()
stat.st_size       # Size in bytes
stat.st_mtime      # Modification time (timestamp)
stat.st_ctime      # Creation time
stat.st_mode       # Permissions
 
# Human-readable time
modified = datetime.fromtimestamp(stat.st_mtime)
 
# Without following symlinks
p.lstat()

Rename and Move

from pathlib import Path
 
src = Path('old.txt')
dst = Path('new.txt')
 
# Rename (same directory)
src.rename(dst)
 
# Move to another directory
src.rename(Path('archive') / src.name)
 
# Replace if exists
src.replace(dst)  # Atomically replaces dst

Practical Patterns

Find Latest File

from pathlib import Path
 
def latest_file(directory, pattern='*'):
    files = Path(directory).glob(pattern)
    return max(files, key=lambda p: p.stat().st_mtime, default=None)
 
latest_log = latest_file('/var/log', '*.log')

Safe File Write

from pathlib import Path
import tempfile
 
def safe_write(path, content):
    """Write atomically using temp file."""
    path = Path(path)
    temp = path.with_suffix('.tmp')
    temp.write_text(content)
    temp.replace(path)  # Atomic on most systems

Backup Before Modify

from pathlib import Path
from datetime import datetime
 
def backup(path):
    path = Path(path)
    if path.exists():
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_path = path.with_suffix(f'.{timestamp}.bak')
        path.rename(backup_path)
        return backup_path
    return None

Clean Directory

from pathlib import Path
import shutil
 
def clean_directory(directory, keep_dir=True):
    d = Path(directory)
    if d.is_dir():
        shutil.rmtree(d)
        if keep_dir:
            d.mkdir()

Walk Directory Tree

from pathlib import Path
 
def walk(directory):
    """Like os.walk but with Path objects."""
    root = Path(directory)
    dirs = []
    files = []
    
    for item in root.iterdir():
        if item.is_dir():
            dirs.append(item)
        else:
            files.append(item)
    
    yield root, dirs, files
    
    for d in dirs:
        yield from walk(d)

Find Duplicates

from pathlib import Path
from collections import defaultdict
import hashlib
 
def find_duplicates(directory):
    hashes = defaultdict(list)
    
    for path in Path(directory).rglob('*'):
        if path.is_file():
            h = hashlib.md5(path.read_bytes()).hexdigest()
            hashes[h].append(path)
    
    return {h: paths for h, paths in hashes.items() if len(paths) > 1}

Project Root Finder

from pathlib import Path
 
def find_project_root(marker='.git'):
    """Find project root by looking for marker file/dir."""
    current = Path.cwd()
    
    for parent in [current] + list(current.parents):
        if (parent / marker).exists():
            return parent
    
    return current

Cross-Platform

from pathlib import Path, PurePosixPath, PureWindowsPath
 
# Current platform
p = Path('file.txt')
 
# Force specific style (for parsing, not I/O)
posix = PurePosixPath('/usr/local/bin')
windows = PureWindowsPath(r'C:\Users\name')
 
# pathlib handles separators automatically
Path('a/b/c')  # Works on Windows too

Summary

pathlib patterns:

  • Construction: Use / operator for joining
  • Reading: read_text(), read_bytes() directly
  • Traversal: iterdir(), glob(), rglob()
  • Modification: with_suffix(), with_name()
  • Safe ops: exist_ok, missing_ok, parents

pathlib makes file operations readable and cross-platform.

React to this post: