Paths look like strings, so people build them with strings: +, "/", split("."). That works until a folder name has a dot in it or the code runs on Windows. pathlib treats a path as a path, and every common question has a named answer.
Before
import os folder = "reports/2026" name = "sales.q3.csv" full = folder + "/" + name base = name.split(".")[0] ext = name.split(".")[-1] backup = full.replace(".csv", ".bak") print(full) print(base, ext) print(backup) print(os.path.dirname(full))
reports/2026/sales.q3.csv sales csv reports/2026/sales.q3.bak reports/2026
base came out as sales, not sales.q3: splitting on the first dot threw part of the name away.
After
from pathlib import PurePosixPath as Path full = Path("reports") / "2026" / "sales.q3.csv" print(full) print(full.stem, full.suffix) print(full.with_suffix(".bak")) print(full.parent) print(full.name)
reports/2026/sales.q3.csv sales.q3 .csv reports/2026/sales.q3.bak reports/2026 sales.q3.csv
In your own code, import Path itself; it picks the right flavour for the operating system. The examples use PurePosixPath so they print the same forward slashes everywhere.
Why it works
/ on a Path means "join", so there's no separator to get wrong. stem is everything before the last dot and suffix is the last dot onwards, which is what you almost always mean. And a real Path does the file work too: .exists(), .read_text(), .mkdir(parents=True) and .glob("*.csv") replace half of os and os.path.
from pathlib import PurePosixPath as Path logs = [Path("logs/app.log"), Path("logs/app.log.1"), Path("logs/archive.tar.gz")] for p in logs: print(p.name, "->", p.suffixes)
app.log -> ['.log'] app.log.1 -> ['.log', '.1'] archive.tar.gz -> ['.tar', '.gz']
When not to use it
Some older libraries still want a plain string; pass str(path) and move on. And for a path that arrives from outside, such as a user's upload name, pathlib tidies it but doesn't make it safe: check it stays inside the folder you expect before you open it.