The Python pathlib module provides Path objects that represent filesystem paths as objects — supporting path joining with the / operator, direct file reading and writing, glob matching, and portable handling of platform path differences.
The clean way to work with file paths
What you'll learn: Path objects, the / operator for joining paths, and the read_text/write_text shortcuts. By the end, pathlib will replace os.path in all your new code.
How to read this tab: Open the REPL and try: from pathlib import Path; p = Path("."); print(list(p.iterdir())). Then try p / "subfolder" / "file.txt". Feel how natural the / operator is.
pathlib turns file paths from plain strings into smart objects — you can join them with /, ask them questions (.exists(), .is_file()), read and write files directly, and never worry about \\ vs / again.
The / operator is pathlib's killer feature. Path("home") / "users" / "priya" / "documents" builds the path correctly on any OS. On Windows it uses backslashes, on macOS/Linux it uses forward slashes. You never have to think about separators again.
Path basics
from pathlib import Path
# Create a Path object
p = Path("/home/priya/data/report.csv")
home = Path.home() # current user's home directory
cwd = Path.cwd() # current working directory
# Join paths with / operator
config = home / "projects" / "myapp" / "config.json"
print(config) # /home/priya/projects/myapp/config.json
# Path components
print(p.name) # report.csv
print(p.stem) # report
print(p.suffix) # .csv
print(p.parent) # /home/priya/data
print(p.parts) # ('/', 'home', 'priya', 'data', 'report.csv')
# Test what the path is
print(p.exists()) # True/False
print(p.is_file()) # True if file
print(p.is_dir()) # True if directoryread_text() and write_text() are one-liners. For simple file reads/writes where you just need the whole content: text = Path("file.txt").read_text(encoding="utf-8"). No with statement, no f.read(), no close(). For large files that need streaming, use open() with a with block as usual.
Reading and writing files
from pathlib import Path
# Read entire file as string
text = Path("readme.md").read_text(encoding="utf-8")
# Read as bytes
data = Path("image.png").read_bytes()
# Write text (creates file, overwrites if exists)
Path("output.txt").write_text("Hello, World!\n", encoding="utf-8")
# Write bytes
Path("data.bin").write_bytes(b"\x00\x01\x02")
# For line-by-line or large files, use open()
with Path("large.csv").open("r", encoding="utf-8") as f:
for line in f:
process(line.strip())rglob() replaces os.walk(). Finding all .py files: list(Path(".").rglob("*.py")). Finding all .csv files in a specific directory: list(Path("data").glob("*.csv")). glob() searches in the current directory; rglob() searches recursively. Both return generators — wrap in list() to materialise all results.
Listing and searching
from pathlib import Path
base = Path("/home/priya/projects")
# List immediate children
for item in base.iterdir():
print(item)
# Glob — find files matching a pattern
for py_file in base.glob("*.py"): # direct children only
print(py_file)
for py_file in base.rglob("*.py"): # recursive — all descendants
print(py_file)
# Find all CSV files modified recently
import time
cutoff = time.time() - 86400 # 24 hours ago
recent = [p for p in base.rglob("*.csv")
if p.stat().st_mtime > cutoff]
# Create directories
(base / "new_dir" / "subdir").mkdir(parents=True, exist_ok=True)
# Rename and delete
old = Path("old_name.txt")
new = old.with_name("new_name.txt") # same directory, new name
old.rename(new)
Path("unwanted.txt").unlink(missing_ok=True) # delete file, no error if missing✅ Beginner tab complete — check your understanding
- I can build paths with the / operator: Path("data") / "users" / "profile.json"
- I can check existence with p.exists(), p.is_file(), p.is_dir()
- I can read a file in one line: Path("file.txt").read_text(encoding="utf-8")
- I can write a file in one line: Path("file.txt").write_text("content", encoding="utf-8")
Listing, searching, and resolving paths
What you'll learn: iterdir() for listing a directory. glob() and rglob() for pattern-based search. resolve() for absolute paths. relative_to() for computing relative paths.
How to read this tab: Try Path(".").rglob("*.py") to find all Python files in your project recursively. This replaces a complex os.walk() loop with one elegant line.
pathlib for new code, os.path for existing code. Both are valid but pathlib is the modern standard. A Path object carries more information than a string and has methods that read naturally. The one limitation: some older libraries only accept strings — use str(path) or os.fspath(path) to convert.
pathlib vs os.path
Both do the same job. pathlib uses an OOP interface — a Path is an object with methods. os.path uses standalone functions on strings. Prefer pathlib for new code: it is more readable, composable (the / operator), and the Path object carries type information. Use os when you need environment variables, permissions, symlinks, or process-related calls that pathlib does not cover. Path objects can be passed to most functions that accept string paths — they implement __fspath__() (PEP 519).
# os.path style
import os
path = os.path.join(os.path.expanduser("~"), "projects", "app", "config.json")
if os.path.exists(path) and os.path.isfile(path):
with open(path) as f:
data = f.read()
# pathlib style — same thing
from pathlib import Path
path = Path.home() / "projects" / "app" / "config.json"
if path.exists() and path.is_file():
data = path.read_text()
# Converting between them
import os
p = Path("/home/priya/data.csv")
as_string = str(p) # "/home/priya/data.csv"
as_string = os.fspath(p) # same — uses __fspath__
# Path arithmetic
p = Path("report.csv")
json_version = p.with_suffix(".json") # report.json
backup = p.with_name(f"backup_{p.name}") # backup_report.csvresolve() gives you the true absolute path. It resolves symlinks and relative path components (.. and .). path.relative_to("/home/priya") strips the prefix and gives you the part of the path relative to that base. Useful when you need to mirror a directory structure.
Resolving and relative paths
from pathlib import Path
# relative_to — compute relative path
base = Path("/home/priya/projects")
file = Path("/home/priya/projects/app/main.py")
rel = file.relative_to(base)
print(rel) # app/main.py
# resolve — canonicalise (follows symlinks, resolves ..)
p = Path("../data/../data/file.txt")
print(p.resolve()) # absolute path with symlinks resolved
# is_relative_to (Python 3.9+)
print(file.is_relative_to(base)) # True
# samefile — same file even through symlinks
p1 = Path("/tmp/link")
p2 = Path("/home/priya/original")
print(p1.samefile(p2)) # True if they point to same inodePath("~") does not expand the tilde. Path("~/documents") gives a path starting with literal ~. Use Path("~/documents").expanduser() or Path.home() / "documents" to get the actual home directory.path.unlink() only removes files, not directories. To remove a directory, use path.rmdir() (empty only) or shutil.rmtree(path) (with contents). unlink() on a directory raises IsADirectoryError.rglob("*.py") and glob("**/*.py") are equivalent but not the same call. path.rglob("*.py") is shorthand for path.glob("**/*.py"). Both search recursively.✅ Intermediate tab complete — check your understanding
- I can list all Python files recursively with list(Path(".").rglob("*.py"))
- I can use glob() for non-recursive search with wildcards
- I can use resolve() to get the absolute path and follow symlinks
- I can compute a relative path with path.relative_to(base)
PEP 428, the Path hierarchy, and os.fspath()
What you'll learn: The pathlib design philosophy (PEP 428). The Path class hierarchy (PurePath → Path → PosixPath/WindowsPath). PEP 519 and os.fspath() for accepting both str and Path objects.
How to read this tab: Read when writing library code that should accept both str and Path arguments for file paths.
PurePath vs Path: PurePath provides path manipulation without any I/O — useful on systems where the path might not exist (building paths for remote systems, testing). Path is PurePath plus actual I/O operations. On import, Path resolves to PosixPath on Unix and WindowsPath on Windows automatically.
PEP 428 and the Path hierarchy
PEP 428 (Python 3.4, 2013) introduced pathlib. The class hierarchy: PurePath (no I/O, just path manipulation) → PurePosixPath / PureWindowsPath; Path(PurePath) (adds I/O) → PosixPath / WindowsPath. Path() automatically instantiates the platform-appropriate concrete class. PurePath objects are useful for path manipulation in cross-platform code (e.g. building Windows paths on a Linux machine) without requiring the path to exist.
PEP 519 — os.fspath() and __fspath__
PEP 519 (Python 3.6, 2016) defined the "file system path protocol." Any object implementing __fspath__() that returns a str or bytes is a "path-like object." os.fspath(p) calls p.__fspath__(). All Python stdlib functions accepting file paths accept path-like objects. This is why open(Path("file.txt")) works.
✅ Expert tab complete
- I know pathlib provides both PurePath (no I/O) and Path (with I/O) classes
- I can use os.fspath() or str() to convert a Path to a string for APIs that don't accept Path objects