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. Compare Path objects, 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.

Frequently Asked Questions

Why use pathlib instead of os.path? Paths become objects with useful properties, the / operator joins them readably, and the same code works on Windows and Unix. os.path works but is more verbose and string-based.
How do I get a path relative to my script? Path(__file__).parent gives the directory containing the script. Building from there avoids depending on the current working directory, which changes with how the script is launched.
What is the difference between glob and rglob? glob searches one directory level; rglob searches recursively through all subdirectories. Both return generators of Path objects.
Can I still use open() with a Path? Yes. Path objects work anywhere a filename is accepted in modern Python, and p.open() is available as a method. Use str(p) for older libraries that insist on strings.
Is read_text suitable for large files? No, it loads the whole file into memory. Use p.open() and iterate line by line for large files, which keeps memory usage constant.