Back to the blog

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/november

The 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, skipped

A 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.