pathlib (3.4+) gives you Path objects with methods for every path operation. os.path is functions taking strings; pathlib is methods on objects. The difference matters because path objects compose cleanly (p / "sub" / "file.txt") and read like English (p.exists(), p.is_dir(), p.glob("*.py")).
Path is the main class — and it's smart
Calling Path("some/path") returns an instance of either PosixPath or WindowsPath depending on your OS. Both share the same API. The / operator joins paths regardless of whether the right side has trailing/leading slashes. Path.home() and Path.cwd() are shortcuts you'll use a lot.
The query methods
Every check you'd do via os.path has a Path equivalent: .exists(), .is_file(), .is_dir(), .is_symlink(). They all return bools. .stat() returns the system stat info. .suffix, .stem, .name, .parent, .parts let you split a path without string manipulation.
Reading and writing — the convenience methods
p.read_text(encoding='utf-8') opens, reads, closes. p.write_text(content, encoding='utf-8') opens, writes, closes. p.read_bytes() / p.write_bytes() for binary. These are perfect for small files where you'd otherwise write a four-line with open() block.
Globbing — finding files by pattern
p.glob("*.py") returns all Python files in p. p.rglob("*.py") recurses. Both return iterators of Path objects. Pattern syntax: * any chars, ** any directories, ? single char, [abc] character class.
Pythonic Way: Default to pathlib for new code. Mix with os.path only where you have to — many older libraries and APIs accept strings, and str(Path(...)) bridges them. Don't go halfway: pick pathlib for the path layer and stick with it consistently.
Code
Path basics — construction and the / operator·python
from pathlib import Path
# Construction
p = Path("/Users/you_username")
print(p) # /Users/you_username
# Joining — / operator
file_path = p / "Documents" / "notes.txt"
print(file_path) # /Users/you_username/Documents/notes.txt
# Useful starting points
print(Path.home()) # /Users/you_username
print(Path.cwd()) # current working directory
print(Path(".").resolve()) # current dir, absolute
from pathlib import Path
p = Path("/etc/hostname")
print(p.exists()) # True
print(p.is_file()) # True
print(p.is_dir()) # False
# Parts and decomposition
q = Path("/etc/profile.d/00-aliases.sh")
print(q.name) # '00-aliases.sh'
print(q.stem) # '00-aliases' — name without suffix
print(q.suffix) # '.sh'
print(q.parent) # PosixPath('/etc/profile.d')
print(q.parts) # ('/', 'etc', 'profile.d', '00-aliases.sh')
# Stat
import time
stat = p.stat()
print("size:", stat.st_size)
print("modified:", time.ctime(stat.st_mtime))
read_text / write_text — the shortcuts·python
from pathlib import Path
p = Path("/tmp/note.txt")
# Write
p.write_text("hello pippa\n", encoding="utf-8")
# Read
content = p.read_text(encoding="utf-8")
print(content)
# Bytes
b = Path("/tmp/raw.bin")
b.write_bytes(b"\xDE\xAD\xBE\xEF")
print(b.read_bytes()) # b'\xde\xad\xbe\xef'
glob and rglob — pattern matching·python
from pathlib import Path
# All .py files in a directory (non-recursive)
for py in Path(".").glob("*.py"):
print(py)
# Recursive — every .py under cwd
for py in Path(".").rglob("*.py"):
print(py)
# Pattern variations
# * any chars (no /)
# ** any directories (with rglob)
# ? single char
# [abc] any of a, b, c
# Use list() if you want to count or sort
files = sorted(Path(".").glob("*.txt"))
print(f"{len(files)} files")
Common operations — mkdir, rename, unlink·python
from pathlib import Path
# Create a directory (parents=True creates intermediates, exist_ok suppresses errors)
d = Path("/tmp/nested/dir")
d.mkdir(parents=True, exist_ok=True)
print(d.exists()) # True
# Create a file inside it
(d / "child.txt").write_text("hi", encoding="utf-8")
# Rename
original = d / "child.txt"
renamed = original.rename(d / "renamed.txt")
print(renamed) # /tmp/nested/dir/renamed.txt
# Delete a file
renamed.unlink(missing_ok=True)
print(renamed.exists()) # False
# Remove an empty directory
d.rmdir() # raises if not empty
# For non-empty trees: shutil.rmtree(d)
Using pathlib: (a) Create a directory /tmp/quest_dir if it doesn't exist. (b) Inside it, write three text files (a.txt, b.txt, c.txt) with different contents. (c) Use glob('*.txt') to iterate them, print the name and the file size. (d) Use rglob from /tmp and verify your three files are found. (e) Clean up — delete the three files and the directory.
Progress
Progress is local-only — sign in to sync across devices.