The Quiet Terminal

← Practical Tips

Four ways to batch-rename files (and the one I use)

2026-06-12 · 3 min read

My screenshots folder currently holds 200-odd files with names like Screen Shot 2026-06-01 at 10.14.33.PNG. They're unique, they sort into chaos, and renaming them by hand is a lost afternoon. Bulk renaming is one of those jobs every scripting language claims to do, so here are the four approaches I've actually used, roughly in the order I learned them — worst to best.

Way 1: the manual loop

The version everyone writes first, including me:

import os

folder = "screenshots"
shots = sorted(n for n in os.listdir(folder) if n.endswith(".png"))
for i, name in enumerate(shots, start=1):
    os.rename(os.path.join(folder, name), os.path.join(folder, f"shot_{i:03d}.png"))

It works. The costs are quiet: os.path.join twice per line, the {i:03d} padding trick to remember, and one stray non-screenshot file shifts every number in the folder. This version is still living in a script from 2019 somewhere on an old drive of mine, and it's fine — just noisy.

Way 2: zip the old names to the new names

The next step up is separating deciding the new names from doing the renaming:

import os

folder = "screenshots"
old_names = sorted(n for n in os.listdir(folder) if n.endswith(".png"))
new_names = (f"shot_{i:03d}.png" for i in range(1, len(old_names) + 1))

for src, dst in zip(old_names, new_names):
    os.rename(os.path.join(folder, src), os.path.join(folder, dst))

The gain is that the pairs now exist before anything moves, so you can print them and look at them first. And zip stops at the shorter list, which means a bug in the name generator can't rename more files than you have. Still, all that join noise remains.

Way 3: let pathlib do the path work

Path objects take over the joining and give you rename methods that read like English:

from pathlib import Path

# the extension swap:
for f in Path("invoices").glob("*.jpeg"):
    f.rename(f.with_suffix(".jpg"))

# and a full cleanup, since my screenshots are named by an operating
# system that apparently hates me:
for f in Path("screenshots").glob("*.PNG"):
    clean = f.with_name(f.stem.lower().replace(" ", "-") + f.suffix.lower())
    f.rename(clean)

with_suffix() swaps the extension in one call. For anything more creative, f.stem gives you the name without the extension and f.with_name() swaps the whole filename:

One scar to save you: my first attempt ran with_suffix(".png") on the cleaned name, and it silently turned 10.14.33 into 10.14 — pathlib counts the .33 in the timestamp as an extension and replaces it. Gluing the suffix on by hand, as above, sidesteps the whole question.

Way 4: dry-run first, always

Renames have no undo. A script that moves files doesn't use the trash folder, so a wrong pattern means restoring by hand. That's why the version I actually use prints its plan and does nothing else until I say otherwise:

from pathlib import Path

RENAME = False   # flip to True when the list looks right

for f in Path("screenshots").glob("*.PNG"):
    clean = f.with_name(f.stem.lower().replace(" ", "-") + f.suffix.lower())
    if clean == f:
        continue
    if RENAME:
        f.rename(clean)
    else:
        print(f.name, "->", clean.name)

Run it and you get the plan, not the damage:

Screen Shot 2026-06-01 at 10.14.33.PNG -> screen-shot-2026-06-01-at-10.14.33.png
Screen Shot 2026-06-01 at 10.21.07.PNG -> screen-shot-2026-06-01-at-10.21.07.png

The one I use

Way 4, every time. The flag costs one line and buys the confidence to point the script at a real folder full of real files instead of a test copy. The zip version is my fallback when I need numbered names, and the plain os version survives only in old code that hasn't needed me yet.