Why pathlib
pathlib gives you:
- operator-free path joining
- clear methods (
exists,is_file,mkdir,read_text) - cross-platform behavior
Before pathlib, paths were plain strings joined with os.path.join or, worse, with + "/" +. That breaks on Windows, where the separator is a backslash, and it makes it easy to build a path that does not point anywhere. A Path object knows the rules of the operating system it runs on. You join parts with /, and you ask the object questions instead of slicing strings.
Automation scripts spend most of their time on paths, so this is the single most useful habit to learn early.
Common patterns
from pathlib import Path
base = Path.home() / "Downloads"
print(base)
for f in base.iterdir():
if f.is_file():
print(f.name, f.suffix)Reading and writing
from pathlib import Path
p = Path("hello.txt")
p.write_text("hello\n", encoding="utf-8")
print(p.read_text(encoding="utf-8"))๐งช Try It Yourself
Exercise 1 โ List Files with os.listdir
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 โ Join Paths with os.path.join
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 โ Write and Read a File
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Taking a path apart
A path has named parts. You rarely need string slicing.
from pathlib import PurePosixPath
p = PurePosixPath("/home/ana/reports/sales.2026.csv")
print(p.name) # sales.2026.csv
print(p.stem) # sales.2026
print(p.suffix) # .csv
print(p.parent) # /home/ana/reports
print(p.with_suffix(".json")) # /home/ana/reports/sales.2026.json
print(p.with_name("costs.csv")) # /home/ana/reports/costs.csvPurePosixPath only manipulates text and never touches the disk, so it is safe to experiment with anywhere. In real scripts you use Path, which adds the methods that do touch the disk. p.parts gives you a tuple of every component.
Useful methods
| Task | Method |
|---|---|
| Does it exist? | p.exists(), p.is_file(), p.is_dir() |
| Create a folder | p.mkdir(parents=True, exist_ok=True) |
| List a folder | p.iterdir() |
| Find files by pattern | p.glob("*.txt"), p.rglob("*.txt") (recursive) |
| Absolute path | p.resolve() |
| Rename or move | p.rename(new_path) |
| Delete a file | p.unlink() |
| Size and time | p.stat().st_size, p.stat().st_mtime |
Common mistakes
- Forgetting that
/joins paths.Path("a") / "b"works.Path("a") + "b"raises aTypeError. - Using a relative path and assuming the working directory.
Path("data.csv")is relative to wherever the script was launched, which is different under cron or Task Scheduler. Build paths fromPath(__file__).parentwhen the file sits next to the script. - Forgetting
encoding.read_text()withoutencoding="utf-8"uses the system default, which differs between machines. - An absolute right-hand side.
Path("a") / "/etc"gives/etc, because an absolute right side replaces the left. - Calling
unlink()orrename()without checking.renamecan overwrite silently on some systems. Checkexists()first.
Adapting it
To process every CSV in a downloads folder, combine what you have seen: for f in (Path.home() / "Downloads").glob("*.csv"), then use f.stem and f.with_suffix() to build output names. The same three or four calls cover most file-handling scripts.