What you'll learn
Quick Answer
Path objects join with the / operator, expose parts like .stem and .suffix as properties, and provide read_text, write_text, mkdir and glob directly. They work identically on Windows and Unix.
What was wrong with strings
The old approach concatenates:
path = folder + "/" + subfolder + "/" + filename # breaks on Windows
import os
path = os.path.join(folder, subfolder, filename) # better, but verbose
Then extracting parts means more calls — os.path.splitext, os.path.basename, os.path.dirname — each returning strings you have to keep straight.
pathlib makes a path an object that knows about itself:
from pathlib import Path
p = Path("demo_dir") / "sub" / "file.txt"
print(p.as_posix()) # demo_dir/sub/file.txt
The / operator joins path segments, using the correct separator for the platform. It reads like a path and cannot produce a doubled or missing slash.
Getting the parts
print(p.suffix) # .txt
print(p.stem) # file
print(p.name) # file.txt
print(p.parent.as_posix()) # demo_dir/sub
Properties rather than function calls, and the names are memorable: name is the filename, stem is it without the extension, suffix is the extension including the dot, parent is the containing directory.
Changing an extension becomes obvious rather than a slicing exercise:
print(p.with_suffix(".md").as_posix()) # demo_dir/sub/file.md
The old equivalent — os.path.splitext(path)[0] + ".md" — works but has to be re-derived every time you read it.
Reading, writing and creating
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("hello from pathlib\n")
print(repr(p.read_text())) # 'hello from pathlib\n'
print(p.exists(), p.is_file()) # True True
parents=True creates intermediate directories, and exist_ok=True makes it safe to call when the directory already exists — together they replace the usual check-then-create dance.
read_text() and write_text() handle opening and closing for you, which is ideal for small files. For large files or line-by-line processing, use p.open(), which returns a normal file object usable with with — see context managers.
Also useful: p.unlink() deletes a file, p.stat().st_size gives its size, and p.resolve() turns a relative path into an absolute one.
Finding files
print([x.name for x in Path("demo_dir").rglob("*.txt")])
# ['file.txt']
glob("*.txt") searches one directory; rglob searches recursively through subdirectories. Both return Path objects, so you can chain straight into the properties above.
This replaces os.walk for most purposes:
for f in Path("src").rglob("*.py"):
if f.stat().st_size > 10_000:
print(f, f.stat().st_size)
Both return generators, so a large tree is not loaded into memory at once. Wrap in sorted() if you need a deterministic order — filesystem order is not guaranteed, which is a common source of tests that pass locally and fail elsewhere.
Practical notes
- Use
__file__for paths relative to your script.Path(__file__).parent / "data.csv"works regardless of the current working directory, which is the usual reason a script works when run from one folder and not another. - Most standard library functions accept Path objects directly, including
open(). Where something old insists on a string,str(p)converts. - Comparing paths as strings is unreliable —
"a/b"and"a//b"and"./a/b"refer to the same place. ComparePathobjects, or call.resolve()first. - Never build a path from user input without validating it. A value containing
../can escape the directory you intended. Resolve the path and confirm it is still inside the expected parent.
pathlib has been in the standard library since Python 3.4 and is the recommended approach in current Python. There is no reason to start new code with os.path.
