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 existsDirectory 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 globsPath 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 symlinksDirectory 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 dstPractical 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 systemsBackup 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 NoneClean 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 currentCross-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 tooSummary
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: