Skip to content

Python Pathlib: The Modern Guide to File Path Handling

Updated on

If you have ever written Python code like os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'output'), you already know the problem. String-based file path manipulation with os.path is verbose, hard to read, and error-prone. You concatenate strings and forget separators. You hardcode / and the script breaks on Windows. You chain five os.path calls together just to get a file's stem name, and three months later nobody can read the code -- including you.

These are not edge cases. Every data science pipeline, every web application, and every automation script touches the file system. Paths that work on your Mac fail on a coworker's Windows laptop. Temporary path variables accumulate in your code like technical debt. And the more os.path.join(os.path.dirname(...)) calls you nest, the more likely you are to introduce a subtle bug that only surfaces in production.

Python's pathlib module solves this. Introduced in Python 3.4 and fully mature since Python 3.6, pathlib replaces string-based path manipulation with proper Path objects. Paths join with the / operator. File attributes like .name, .suffix, and .stem are properties, not function calls. Reading and writing files takes one line. And everything works identically across operating systems. This guide covers every essential pathlib feature, from basic path construction to advanced patterns for data science workflows.

📚

Why pathlib Over os.path

Before pathlib, Python developers relied on os.path for path operations and os for file system interactions. That approach works, but it treats paths as plain strings. This creates three persistent problems:

  1. Readability degrades fast. Compare os.path.splitext(os.path.basename(filepath))[0] with Path(filepath).stem. Both extract the filename without its extension. One is self-documenting; the other requires mental parsing.

  2. Cross-platform bugs. Hardcoding / as a separator or using string concatenation means your Linux script silently breaks on Windows. os.path.join helps, but forgetting to use it even once creates a latent bug.

  3. Scattered functionality. To work with paths you need os.path for decomposition, os for directory creation, glob for pattern matching, and open() for file I/O. pathlib consolidates all of this into a single Path object.

Here is the same task -- find all .csv files in a data directory and read the first one -- in both styles:

# os.path approach
import os
import glob
 
data_dir = os.path.join(os.path.expanduser('~'), 'projects', 'data')
csv_files = glob.glob(os.path.join(data_dir, '**', '*.csv'), recursive=True)
if csv_files:
    with open(csv_files[0], 'r') as f:
        content = f.read()
# pathlib approach
from pathlib import Path
 
data_dir = Path.home() / 'projects' / 'data'
csv_files = list(data_dir.rglob('*.csv'))
if csv_files:
    content = csv_files[0].read_text()

The pathlib version is shorter, easier to read, and does exactly the same thing. No imports beyond Path. No string concatenation. No separate open() call.

Creating Path Objects

Every pathlib operation starts with creating a Path object. The Path class automatically returns a PosixPath on Linux/macOS or a WindowsPath on Windows.

from pathlib import Path
 
# From a string
p = Path('/home/user/documents/report.csv')
 
# From multiple segments (joined automatically)
p = Path('home', 'user', 'documents', 'report.csv')
 
# Current working directory
cwd = Path.cwd()
print(cwd)  # e.g., /home/user/projects/myapp
 
# User home directory
home = Path.home()
print(home)  # e.g., /home/user
 
# Relative path
p = Path('data/output/results.csv')
 
# From an existing path
base = Path('/home/user')
full = Path(base, 'documents', 'file.txt')
print(full)  # /home/user/documents/file.txt

Path() with no arguments returns Path('.'), a relative path to the current directory. Use Path.cwd() when you need the absolute current directory.

Joining Paths with the / Operator

The most distinctive feature of pathlib is its overloaded / operator. Instead of os.path.join(), you chain path segments with /:

from pathlib import Path
 
# Build paths naturally
project = Path.home() / 'projects' / 'analysis'
data_file = project / 'data' / 'sales_2026.csv'
print(data_file)  # /home/user/projects/analysis/data/sales_2026.csv
 
# Mix Path objects and strings
base = Path('/var/log')
app_log = base / 'myapp' / 'error.log'
print(app_log)  # /var/log/myapp/error.log
 
# Combine with variables
filename = 'report.pdf'
output = Path('output') / filename
print(output)  # output/report.pdf

The / operator handles separators automatically. On Windows, Path('C:/Users') / 'data' produces C:\Users\data. You never need to think about / vs \ again.

You can also use joinpath() for the same result:

from pathlib import Path
 
# Equivalent to Path('data') / 'raw' / 'file.csv'
p = Path('data').joinpath('raw', 'file.csv')
print(p)  # data/raw/file.csv

Path Components

Every Path object exposes its components as properties. No function calls, no string splitting.

from pathlib import Path
 
p = Path('/home/user/projects/analysis/data/sales_report.final.csv')
 
print(p.name)       # sales_report.final.csv  (filename with extension)
print(p.stem)       # sales_report.final      (filename without last extension)
print(p.suffix)     # .csv                    (last extension)
print(p.suffixes)   # ['.final', '.csv']      (all extensions)
print(p.parent)     # /home/user/projects/analysis/data
print(p.anchor)     # /                       (root on Unix, C:\ on Windows)
print(p.parts)      # ('/', 'home', 'user', 'projects', 'analysis', 'data', 'sales_report.final.csv')

Navigating Parents

The .parent property returns the immediate parent directory. Chain it to go higher:

from pathlib import Path
 
p = Path('/home/user/projects/analysis/data/output.csv')
 
print(p.parent)            # /home/user/projects/analysis/data
print(p.parent.parent)     # /home/user/projects/analysis
print(p.parent.parent.parent)  # /home/user/projects
 
# .parents gives indexed access to all ancestors
print(p.parents[0])  # /home/user/projects/analysis/data
print(p.parents[1])  # /home/user/projects/analysis
print(p.parents[2])  # /home/user/projects
print(p.parents[3])  # /home/user

Changing Path Components

Use .with_name(), .with_stem(), and .with_suffix() to create new paths with modified components:

from pathlib import Path
 
p = Path('/data/reports/sales_q1.csv')
 
# Change the filename entirely
print(p.with_name('revenue_q1.csv'))    # /data/reports/revenue_q1.csv
 
# Change only the stem (Python 3.9+)
print(p.with_stem('sales_q2'))          # /data/reports/sales_q2.csv
 
# Change only the extension
print(p.with_suffix('.parquet'))        # /data/reports/sales_q1.parquet
 
# Remove the extension
print(p.with_suffix(''))                # /data/reports/sales_q1
 
# Add an extension
backup = p.with_suffix(p.suffix + '.bak')
print(backup)                           # /data/reports/sales_q1.csv.bak

These methods return new Path objects. They do not rename files on disk.

File I/O: Reading and Writing

pathlib eliminates the open() / with boilerplate for simple file operations:

from pathlib import Path
 
file_path = Path('example.txt')
 
# Write text to a file (creates if not exists, overwrites if exists)
file_path.write_text('Hello, pathlib!\nSecond line.')
 
# Read text from a file
content = file_path.read_text()
print(content)
# Hello, pathlib!
# Second line.
 
# Write bytes
binary_path = Path('data.bin')
binary_path.write_bytes(b'\x00\x01\x02\x03')
 
# Read bytes
raw = binary_path.read_bytes()
print(raw)  # b'\x00\x01\x02\x03'

Specify encoding explicitly when working with non-ASCII text:

from pathlib import Path
 
# Write UTF-8 text
Path('greeting.txt').write_text('こんにちは世界', encoding='utf-8')
 
# Read with encoding
text = Path('greeting.txt').read_text(encoding='utf-8')
print(text)  # こんにちは世界

For large files or streaming operations, use .open() which returns a file handle just like the built-in open():

from pathlib import Path
 
log_file = Path('application.log')
 
# Write line by line
with log_file.open('w') as f:
    for i in range(1000):
        f.write(f'Event {i}: processed\n')
 
# Read line by line (memory-efficient for large files)
with log_file.open('r') as f:
    for line in f:
        if 'error' in line.lower():
            print(line.strip())

Directory Operations

Creating Directories

from pathlib import Path
 
# Create a single directory
Path('output').mkdir()
 
# Create with parents (like os.makedirs)
Path('data/raw/2026/february').mkdir(parents=True, exist_ok=True)
 
# parents=True creates all missing parent directories
# exist_ok=True prevents error if directory already exists

A common mistake is forgetting parents=True. Without it, mkdir() raises FileNotFoundError if any parent directory is missing. Always use parents=True when creating nested directories, and exist_ok=True to make the operation idempotent.

Listing Directory Contents

from pathlib import Path
 
project = Path('.')
 
# List all entries (files and directories)
for entry in project.iterdir():
    print(entry.name, '(dir)' if entry.is_dir() else '(file)')
 
# Filter to files only
files = [f for f in project.iterdir() if f.is_file()]
print(f"Found {len(files)} files")
 
# Filter to directories only
dirs = [d for d in project.iterdir() if d.is_dir()]
print(f"Found {len(dirs)} directories")
 
# Sort by name
for entry in sorted(project.iterdir()):
    print(entry.name)

Removing Directories and Files

from pathlib import Path
 
# Remove a file
Path('temp_output.csv').unlink()
 
# Remove a file only if it exists (Python 3.8+)
Path('temp_output.csv').unlink(missing_ok=True)
 
# Remove an empty directory
Path('empty_dir').rmdir()

rmdir() only removes empty directories. For non-empty directories, use shutil.rmtree():

from pathlib import Path
import shutil
 
target = Path('data/old_output')
if target.exists():
    shutil.rmtree(target)

Glob Patterns: Finding Files

pathlib has built-in glob support. No need to import the glob module separately.

Basic Glob

from pathlib import Path
 
project = Path('/home/user/project')
 
# Find all Python files in a directory
for py_file in project.glob('*.py'):
    print(py_file.name)
 
# Find all CSV files
csv_files = list(project.glob('*.csv'))
print(f"Found {len(csv_files)} CSV files")
 
# Find files matching a pattern
reports = list(project.glob('report_*.xlsx'))

Recursive Glob with rglob

rglob() searches recursively through all subdirectories. It is equivalent to glob('**/*.pattern') but more convenient:

from pathlib import Path
 
project = Path('/home/user/project')
 
# Find all Python files in all subdirectories
all_py = list(project.rglob('*.py'))
print(f"Found {len(all_py)} Python files across all directories")
 
# Find all Jupyter notebooks recursively
notebooks = list(project.rglob('*.ipynb'))
for nb in notebooks:
    print(f"  {nb.relative_to(project)}")
 
# Find all image files
images = list(project.rglob('*.png')) + list(project.rglob('*.jpg'))
 
# Find all files (no filter)
all_files = [f for f in project.rglob('*') if f.is_file()]

Advanced Glob Patterns

from pathlib import Path
 
data = Path('data')
 
# Single character wildcard
data.glob('file_?.csv')        # file_1.csv, file_a.csv
 
# Character ranges
data.glob('report_202[456].csv')  # report_2024.csv, report_2025.csv, report_2026.csv
 
# Any subdirectory level
data.glob('**/output/*.csv')   # data/raw/output/result.csv, data/processed/output/result.csv
 
# Multiple extensions (combine two globs)
from itertools import chain
all_data = chain(data.rglob('*.csv'), data.rglob('*.parquet'))

Checking Paths

pathlib provides clear, boolean methods for checking path status:

from pathlib import Path
 
p = Path('/home/user/projects/data.csv')
 
# Does the path exist?
print(p.exists())       # True or False
 
# Is it a file?
print(p.is_file())      # True if exists and is a regular file
 
# Is it a directory?
print(p.is_dir())       # True if exists and is a directory
 
# Is it a symbolic link?
print(p.is_symlink())   # True if exists and is a symlink
 
# Is it an absolute path?
print(p.is_absolute())  # True (/home/... starts with root)
print(Path('data.csv').is_absolute())  # False (relative path)

These methods never raise exceptions for non-existent paths. They simply return False, which makes them safe to use in conditionals:

from pathlib import Path
 
config = Path('config.yaml')
if config.is_file():
    settings = config.read_text()
else:
    print("Config file not found, using defaults")

Path Manipulation

Resolving and Normalizing Paths

from pathlib import Path
 
# Resolve to absolute path (also resolves symlinks)
p = Path('data/../data/./output.csv')
print(p.resolve())  # /home/user/project/data/output.csv
 
# Get absolute path without resolving symlinks
print(p.absolute())  # /home/user/project/data/../data/./output.csv
 
# Expand user home directory
p = Path('~/Documents/report.csv')
print(p.expanduser())  # /home/user/Documents/report.csv

Relative Paths

from pathlib import Path
 
full_path = Path('/home/user/projects/analysis/data/output.csv')
base = Path('/home/user/projects')
 
# Get the relative path from base to full_path
relative = full_path.relative_to(base)
print(relative)  # analysis/data/output.csv
 
# This raises ValueError if the path is not relative to the base
try:
    Path('/var/log/app.log').relative_to(base)
except ValueError as e:
    print(e)  # '/var/log/app.log' is not relative to '/home/user/projects'
 
# Python 3.12+: is_relative_to() check
print(full_path.is_relative_to(base))   # True
print(Path('/var/log').is_relative_to(base))  # False

File Metadata and Stat

from pathlib import Path
from datetime import datetime
 
p = Path('data.csv')
 
# Get file stats
stat = p.stat()
print(f"Size: {stat.st_size} bytes")
print(f"Modified: {datetime.fromtimestamp(stat.st_mtime)}")
print(f"Created: {datetime.fromtimestamp(stat.st_ctime)}")
 
# Convenience: get size directly (through stat)
size_mb = p.stat().st_size / (1024 * 1024)
print(f"Size: {size_mb:.2f} MB")
 
# Check if two paths point to the same file
p1 = Path('/home/user/data.csv')
p2 = Path.home() / 'data.csv'
print(p1.samefile(p2))  # True (if they resolve to the same file)

Renaming and Moving Files

from pathlib import Path
 
# Rename a file (returns the new Path)
old = Path('report_draft.csv')
new = old.rename('report_final.csv')
print(new)  # report_final.csv
 
# Move to a different directory
source = Path('output/temp_results.csv')
dest = source.rename(Path('archive') / source.name)
 
# Replace a file (overwrites if destination exists)
Path('new_data.csv').replace('data.csv')

Note: .rename() will overwrite the destination file on Unix but may raise an error on Windows. Use .replace() for guaranteed cross-platform overwrite behavior.

os.path vs pathlib: Complete Comparison

Here is a reference table mapping every common os.path operation to its pathlib equivalent:

Operationos.path / ospathlib
Join pathsos.path.join('a', 'b')Path('a') / 'b'
Current directoryos.getcwd()Path.cwd()
Home directoryos.path.expanduser('~')Path.home()
Absolute pathos.path.abspath(p)Path(p).resolve()
Filenameos.path.basename(p)Path(p).name
Directoryos.path.dirname(p)Path(p).parent
Extensionos.path.splitext(p)[1]Path(p).suffix
Stem (name without ext)os.path.splitext(os.path.basename(p))[0]Path(p).stem
Existsos.path.exists(p)Path(p).exists()
Is fileos.path.isfile(p)Path(p).is_file()
Is directoryos.path.isdir(p)Path(p).is_dir()
Is symlinkos.path.islink(p)Path(p).is_symlink()
Is absoluteos.path.isabs(p)Path(p).is_absolute()
File sizeos.path.getsize(p)Path(p).stat().st_size
List directoryos.listdir(p)Path(p).iterdir()
Create directoryos.makedirs(p, exist_ok=True)Path(p).mkdir(parents=True, exist_ok=True)
Remove fileos.remove(p)Path(p).unlink()
Remove directoryos.rmdir(p)Path(p).rmdir()
Renameos.rename(old, new)Path(old).rename(new)
Read fileopen(p).read()Path(p).read_text()
Write fileopen(p, 'w').write(text)Path(p).write_text(text)
Globglob.glob('*.py')Path('.').glob('*.py')
Recursive globglob.glob('**/*.py', recursive=True)Path('.').rglob('*.py')
Expand useros.path.expanduser(p)Path(p).expanduser()
Relative pathos.path.relpath(p, base)Path(p).relative_to(base)

Working with Temporary Files

pathlib integrates cleanly with Python's tempfile module:

from pathlib import Path
import tempfile
 
# Create a temporary directory as a Path
with tempfile.TemporaryDirectory() as tmp_dir:
    tmp_path = Path(tmp_dir)
 
    # Write temporary files using pathlib
    data_file = tmp_path / 'intermediate_results.csv'
    data_file.write_text('col1,col2\n1,2\n3,4\n')
 
    config_file = tmp_path / 'run_config.json'
    config_file.write_text('{"epochs": 100, "lr": 0.001}')
 
    # List what we created
    for f in tmp_path.iterdir():
        print(f"{f.name}: {f.stat().st_size} bytes")
 
    # Process files...
    print(data_file.read_text())
 
# Directory and all files are automatically deleted here
from pathlib import Path
import tempfile
 
# Create a named temporary file
tmp = tempfile.NamedTemporaryFile(suffix='.csv', delete=False)
tmp_path = Path(tmp.name)
tmp.close()
 
# Use pathlib to write to it
tmp_path.write_text('id,value\n1,100\n2,200\n')
print(f"Temp file at: {tmp_path}")
 
# Clean up when done
tmp_path.unlink()

Pathlib in Data Science Workflows

Data science projects typically involve reading datasets from multiple directories, creating output folders for results, and managing experiment artifacts. pathlib makes these patterns clean and reliable.

Organizing Project Directories

from pathlib import Path
 
def setup_experiment(experiment_name):
    """Create a standard experiment directory structure."""
    base = Path('experiments') / experiment_name
 
    dirs = ['data/raw', 'data/processed', 'models', 'results/figures', 'results/tables', 'logs']
 
    for d in dirs:
        (base / d).mkdir(parents=True, exist_ok=True)
 
    # Create a config file
    config = base / 'config.json'
    if not config.exists():
        config.write_text('{"learning_rate": 0.001, "epochs": 50}')
 
    print(f"Experiment directory ready: {base.resolve()}")
    return base
 
project = setup_experiment('sales_forecast_v2')

Reading Multiple Data Files

from pathlib import Path
import pandas as pd
 
data_dir = Path('data/raw')
 
# Read all CSV files into a single DataFrame
dfs = []
for csv_file in sorted(data_dir.glob('*.csv')):
    print(f"Loading {csv_file.name}...")
    df = pd.read_csv(csv_file)
    df['source_file'] = csv_file.stem  # Add source filename
    dfs.append(df)
 
combined = pd.concat(dfs, ignore_index=True)
print(f"Loaded {len(combined)} rows from {len(dfs)} files")
 
# Save to processed directory
output_path = Path('data/processed') / 'combined_sales.parquet'
output_path.parent.mkdir(parents=True, exist_ok=True)
combined.to_parquet(output_path)

After loading your CSV data with pathlib, you can explore it visually with PyGWalker (opens in a new tab). It turns any Pandas DataFrame into a Tableau-like interactive interface for drag-and-drop data exploration -- no extra code required.

Saving Experiment Results

from pathlib import Path
from datetime import datetime
import json
 
def save_results(metrics, experiment_dir):
    """Save experiment metrics with timestamp."""
    results_dir = Path(experiment_dir) / 'results'
    results_dir.mkdir(parents=True, exist_ok=True)
 
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    output_file = results_dir / f'metrics_{timestamp}.json'
 
    output_file.write_text(json.dumps(metrics, indent=2))
    print(f"Results saved to {output_file}")
    return output_file
 
# Usage
metrics = {'accuracy': 0.94, 'f1_score': 0.91, 'loss': 0.187}
save_results(metrics, 'experiments/sales_forecast_v2')

Managing File Paths in Notebooks

When working in Jupyter notebooks, paths often break because the notebook's working directory may differ from the project root. pathlib makes this easy to handle:

from pathlib import Path
 
# Always resolve to absolute path from the notebook location
NOTEBOOK_DIR = Path.cwd()
PROJECT_ROOT = NOTEBOOK_DIR.parent  # if notebook is in notebooks/
DATA_DIR = PROJECT_ROOT / 'data'
OUTPUT_DIR = PROJECT_ROOT / 'output'
 
# Now all paths are absolute and reliable
train_data = DATA_DIR / 'train.csv'
print(f"Loading: {train_data}")
assert train_data.exists(), f"Missing: {train_data}"

If you work extensively in Jupyter and want an AI-powered environment that helps manage project files and data paths, RunCell (opens in a new tab) adds an AI agent layer to your notebook. Describe what you need -- "find all Parquet files in the data directory and load the latest one" -- and it generates the pathlib code and runs it for you.

Common Patterns and Recipes

Safe File Write with Atomic Replacement

Prevent data corruption by writing to a temporary file first, then atomically replacing the target:

from pathlib import Path
import tempfile
 
def safe_write(target_path, content):
    """Write content to file atomically to prevent corruption."""
    target = Path(target_path)
    target.parent.mkdir(parents=True, exist_ok=True)
 
    # Write to temp file in the same directory
    tmp = tempfile.NamedTemporaryFile(
        mode='w', dir=target.parent, suffix='.tmp', delete=False
    )
    tmp_path = Path(tmp.name)
    try:
        tmp.write(content)
        tmp.close()
        tmp_path.replace(target)  # Atomic on most file systems
    except Exception:
        tmp_path.unlink(missing_ok=True)
        raise
 
safe_write('config/settings.json', '{"debug": true}')

Batch File Rename

from pathlib import Path
 
photos_dir = Path('photos')
 
# Rename all .jpeg files to .jpg
for f in photos_dir.glob('*.jpeg'):
    f.rename(f.with_suffix('.jpg'))
 
# Add prefix to all files
for i, f in enumerate(sorted(photos_dir.glob('*.jpg')), start=1):
    new_name = f.parent / f'photo_{i:04d}{f.suffix}'
    f.rename(new_name)

Find Duplicate Files by Size

from pathlib import Path
from collections import defaultdict
 
def find_potential_duplicates(directory):
    """Find files with identical sizes (potential duplicates)."""
    size_map = defaultdict(list)
 
    for f in Path(directory).rglob('*'):
        if f.is_file():
            size_map[f.stat().st_size].append(f)
 
    # Return only groups with more than one file
    return {size: files for size, files in size_map.items() if len(files) > 1}
 
dupes = find_potential_duplicates('data')
for size, files in dupes.items():
    print(f"\n{size} bytes:")
    for f in files:
        print(f"  {f}")

Build a File Tree Visualization

from pathlib import Path
 
def tree(directory, prefix='', max_depth=3, _depth=0):
    """Print a tree structure of a directory."""
    if _depth >= max_depth:
        return
 
    path = Path(directory)
    entries = sorted(path.iterdir(), key=lambda e: (e.is_file(), e.name))
 
    for i, entry in enumerate(entries):
        is_last = (i == len(entries) - 1)
        connector = '└── ' if is_last else '├── '
        print(f'{prefix}{connector}{entry.name}')
 
        if entry.is_dir():
            extension = '    ' if is_last else '│   '
            tree(entry, prefix + extension, max_depth, _depth + 1)
 
tree('my_project', max_depth=3)

Output:

├── data
│   ├── processed
│   │   └── combined.csv
│   └── raw
│       ├── sales_2025.csv
│       └── sales_2026.csv
├── notebooks
│   └── analysis.ipynb
├── output
│   └── figures
└── requirements.txt

Common Mistakes and How to Avoid Them

Mistake 1: Comparing Strings to Path Objects

from pathlib import Path
 
p = Path('data/output.csv')
 
# WRONG: Comparing string to Path
if p == 'data/output.csv':  # May work but fragile
    print("Match")
 
# RIGHT: Compare Path to Path, or use str()
if p == Path('data/output.csv'):
    print("Match")
 
# RIGHT: Convert to string if needed
if str(p) == 'data/output.csv':
    print("Match")

Mistake 2: Forgetting parents=True in mkdir

from pathlib import Path
 
# WRONG: Raises FileNotFoundError if 'data' doesn't exist
# Path('data/raw/2026').mkdir()
 
# RIGHT: Create all missing parents
Path('data/raw/2026').mkdir(parents=True, exist_ok=True)

Mistake 3: Using String Concatenation Instead of /

from pathlib import Path
 
base = Path('/home/user')
 
# WRONG: String concatenation breaks pathlib
# bad = base + '/data/file.csv'  # TypeError
 
# RIGHT: Use the / operator
good = base / 'data' / 'file.csv'

Mistake 4: Passing Path to Libraries That Expect Strings

Most modern libraries (Pandas, NumPy, PIL, etc.) accept Path objects natively. But if you encounter an older library that requires strings, convert explicitly:

from pathlib import Path
 
p = Path('data/output.csv')
 
# Most libraries accept Path directly
import pandas as pd
df = pd.read_csv(p)  # Works fine
 
# For older libraries that need strings
import some_legacy_lib
some_legacy_lib.process(str(p))  # Convert with str()
 
# os.fspath() also works (Python 3.6+)
import os
some_legacy_lib.process(os.fspath(p))

Mistake 5: Using Hardcoded Paths

from pathlib import Path
 
# WRONG: Hardcoded absolute path
# data_path = Path('/home/alice/project/data/sales.csv')
 
# RIGHT: Build from relative or dynamic components
data_path = Path.cwd() / 'data' / 'sales.csv'
 
# RIGHT: Build from home directory
config_path = Path.home() / '.config' / 'myapp' / 'settings.json'
 
# RIGHT: Build from environment variable
import os
data_root = Path(os.getenv('DATA_DIR', 'data'))
data_path = data_root / 'sales.csv'

Frequently Asked Questions

What is pathlib in Python?

pathlib is a standard library module (introduced in Python 3.4) that provides object-oriented classes for working with file system paths. Instead of treating paths as strings and using functions like os.path.join(), you create Path objects and use methods and operators. It handles cross-platform path differences automatically.

When should I use pathlib instead of os.path?

Use pathlib for all new Python 3.6+ projects. It produces cleaner, more readable code, consolidates path operations into a single object, and handles cross-platform issues automatically. The only reason to use os.path is maintaining legacy code that must support Python 2, or using the few os functions that have no pathlib equivalent (like os.environ for environment variables).

Does pathlib work on Windows?

Yes. pathlib automatically uses WindowsPath objects on Windows and PosixPath on Linux/macOS. The / operator produces backslash-separated paths on Windows. You write the same code on all platforms and pathlib handles the differences.

Can I use Path objects with Pandas?

Yes. Since Python 3.6 and Pandas 0.21+, you can pass Path objects directly to pd.read_csv(), pd.read_excel(), df.to_csv(), and other I/O functions. No str() conversion needed.

What is the difference between Path.resolve() and Path.absolute()?

.resolve() returns the absolute path and also resolves any symbolic links and ../. components. .absolute() returns the absolute path without resolving symlinks or normalizing the path. In most cases, .resolve() is what you want.

How do I convert between Path objects and strings?

Use str(path) to convert a Path to a string. Use Path(string) to create a Path from a string. You can also use os.fspath(path) for explicit string conversion. Most modern Python libraries accept Path objects directly, so conversion is rarely necessary.

Conclusion

Python's pathlib module is the modern standard for file path manipulation. The / operator makes path joining readable. Properties like .name, .stem, .suffix, and .parent eliminate verbose os.path function chains. Built-in methods for reading, writing, creating directories, and globbing consolidate what used to require os, os.path, glob, and open() into a single, consistent API.

The migration from os.path to pathlib is straightforward: replace os.path.join() with /, replace os.path.exists() with .exists(), replace os.makedirs() with .mkdir(parents=True), and replace glob.glob() with .glob() or .rglob(). Every major Python library -- Pandas, NumPy, PIL, PyTorch -- now accepts Path objects natively. There is no reason to avoid it in new projects.

Start small. Pick one script that has messy os.path code. Replace the path operations with pathlib. The code will get shorter, more readable, and more portable. Then do the same for the next script.

📚