Tony CantaveHow Python pathlib Made My File Scripts Easier to Read
By Tony Cantave ·
A lot of the scripts I write move files around. Sort a folder of exports by month, find every CSV under a project, rename a batch of screenshots before they go into a report. For years I wrote those with os.path and string concatenation, and I spent too much time on bugs that came from a missing slash or a path built for the wrong operating system.
pathlib fixed that. It has been in the standard library since Python 3.4, and it treats a path as an object with methods instead of a string you have to glue together. Here is how I use it day to day.
Paths as objects, not strings
from pathlib import Path
base = Path("exports")
report = base / "2026" / "november" / "summary.csv"
print(report) # exports/2026/november/summary.csv
print(report.name) # summary.csv
print(report.stem) # summary
print(report.suffix) # .csv
print(report.parent) # exports/2026/novemberThe slash operator joins parts with the right separator for the system the script runs on. On Windows the same code produces backslashes. I no longer think about it.
Compare the older way: os.path.join(base, "2026", "november", "summary.csv"), then os.path.splitext(os.path.basename(path))[0] just to get the stem. It works, but it reads inside out.
Finding files
glob and rglob replace most of the os.walk loops I used to write.
from pathlib import Path
project = Path("data")
for csv_file in project.glob("*.csv"):
print("top level:", csv_file)
for csv_file in project.rglob("*.csv"):
print("anywhere below:", csv_file)glob looks only in the folder itself. rglob walks every subfolder. Both return Path objects, so I can call stat(), name or read_text() on the results right away.
One small habit: I wrap the results in sorted(). File system order is not guaranteed, and sorted output makes logs and tests repeatable.
Reading and writing small files
For small text files I skip the open block entirely.
from pathlib import Path
notes = Path("notes.txt")
notes.write_text("first line\n", encoding="utf8")
content = notes.read_text(encoding="utf8")
print(content.splitlines())I always pass encoding. Leaving it out means the default depends on the machine, which is exactly the kind of difference that works on my laptop and fails somewhere else. For large files I still use a with block around path.open() and read line by line.
Creating folders safely
from pathlib import Path
out_dir = Path("output") / "2026" / "11"
out_dir.mkdir(parents=True, exist_ok=True)parents=True creates every missing folder in the chain, and exist_ok=True means a second run does not crash because the folder is already there. Scripts that can be run twice without harm are scripts I trust.
A real example: sort exports by month
This is close to a script I actually use. It takes files named like sales_2026_11_03.csv and moves them into year and month folders. It also supports a dry run, which I consider required for anything that moves files.
from pathlib import Path
def sort_exports(src, dest, dry_run=True):
src, dest = Path(src), Path(dest)
moved, skipped = 0, []
for f in sorted(src.glob("*.csv")):
parts = f.stem.split("_")
if len(parts) != 4 or not all(p.isdigit() for p in parts[1:]):
skipped.append(f.name)
continue
_, year, month, _ = parts
target_dir = dest / year / month
target = target_dir / f.name
if target.exists():
skipped.append(f.name)
continue
print("WOULD MOVE" if dry_run else "MOVE", f, "to", target)
if not dry_run:
target_dir.mkdir(parents=True, exist_ok=True)
f.rename(target)
moved += 1
return moved, skippedA few details matter here. The name check skips anything that does not match the pattern instead of guessing. The script refuses to overwrite a file that already exists at the target. And nothing moves unless I pass dry_run=False on purpose. One note: rename works within one drive. To move across drives, use shutil.move, which accepts Path objects too.
Resolving paths relative to the script
One bug I hit early: a script worked when I ran it from its own folder and failed when a scheduler ran it from somewhere else. Relative paths are relative to the current working directory, not to the script. The fix is one line:
from pathlib import Path
HERE = Path(__file__).resolve().parent
config = HERE / "config.json"Now config always points next to the script, no matter where it is launched from.
Checks I lean on
- exists(), is_file() and is_dir() before acting on a path
- with_suffix(".bak") to build a backup file name
- stat().st_size and stat().st_mtime for size and last modified time
- relative_to(base) for short, clean log messages
What I took away
Years in logistics and production taught me that clear labels prevent most mistakes on a shipping floor. pathlib does the same for files: every path carries its own name, folder and extension, and the code reads in the same order I think about the task. If your scripts still build paths by adding strings together, try converting one small script. It is usually a twenty minute change that removes a whole class of bugs.