⟳ Automation

Batch rename files with Python: a practical and safe guide

How to batch rename hundreds of files with Python and pathlib: dates in names, numbering, dry runs, name conflicts and a log of every change.

Renaming thirty scans called IMG_0041.jpg, IMG_0042.jpg and so on by hand is tedious but doable. Renaming three hundred, perhaps every month, is exactly the kind of job worth handing to a script. Python, with the standard pathlib module, lets you do it in a few lines and, above all, in a controlled way: first you look at what will happen, then you run it.

Why a script and not the operating system's tool

Windows File Explorer and the macOS Finder both offer bulk renaming, but they are limited: they add a sequential number or replace a piece of text. As soon as the rule gets richer ("put the modification date first, then the client name taken from the folder, then a three-digit number") you need more flexible tools. A script has another advantage too: it is repeatable. The rule you write today will work the same way six months from now.

The basic pattern with pathlib

The heart of every rename job is always the same: list the files, compute the new name, apply it.

from pathlib import Path

folder = Path("C:/Work/Scans")
for i, file in enumerate(sorted(folder.glob("*.jpg")), start=1):
    new = file.with_name(f"invoice_{i:03d}{file.suffix.lower()}")
    print(file.name, "->", new.name)

A few details that make a difference:

  • sorted() guarantees a predictable order; without it, the order depends on the filesystem.
  • {i:03d} produces 001, 002, 010: with leading zeros the files stay correctly ordered even when sorted alphabetically.
  • file.suffix.lower() normalises extensions, so you don't end up with .JPG and .jpg mixed together.

Notice that, for now, the script only prints. That is deliberate.

A dry run before anything else

The first principle of any file automation is to separate the calculation from the action. A DRY_RUN = True variable at the top of the script lets you see the complete list of transformations without touching anything. Only when the list looks right do you switch to False.

DRY_RUN = True

if DRY_RUN:
    print(file.name, "->", new.name)
else:
    file.rename(new)

In my work this habit has prevented more trouble than any other technique: a mistake in the renaming rule shows up immediately in the list, whereas discovering it afterwards means rebuilding the original names by hand.

Using the date in the file name

A very useful format for archiving documents is YYYY-MM-DD_description.ext. With the date in this order, alphabetical sorting matches chronological sorting. Python can derive it from the last modification date:

from datetime import datetime

date = datetime.fromtimestamp(file.stat().st_mtime).strftime("%Y-%m-%d")
new = file.with_name(f"{date}_{file.stem}{file.suffix}")

Be careful, though: the modification date changes if the file is copied with certain tools or edited. For photos, the capture date stored in the EXIF metadata is more reliable, and it can be read with libraries such as Pillow. For documents, the right date is sometimes written in the name itself or in the content: in that case it is best to extract it with a regular expression.

Handling name conflicts

The most serious risk is two files ending up with the same name. On Windows rename() raises an error if the destination exists; on Linux and macOS, however, it silently overwrites the destination file. That is why you should always check:

if new.exists():
    print("Conflict, skipping:", new.name)
    continue

Another solution is to add a sequential suffix (_2, _3) until the name is free. Either way, the conflict must be handled explicitly, not left to chance.

There is also a subtle case: renaming a.txt to b.txt and b.txt to c.txt in the same loop. If the order is wrong, the first step finds b.txt still taken. When rules can overlap, it is better to rename in two passes: first give every file a temporary name, then the final one.

Keeping a log of changes

A well-written script leaves a trace of what it did. A simple CSV file with two columns, old name and new name, lets you undo the operation at any time with a reverse script.

import csv

with open("rename_log.csv", "a", newline="", encoding="utf-8") as f:
    csv.writer(f).writerow([file.name, new.name])

The log is also useful for answering the question someone will ask sooner or later: "where did the file that used to be called this go?".

Characters to avoid in names

When the new name comes from external data, for example a client name taken from a spreadsheet, it needs cleaning. On Windows the characters \ / : * ? " < > | are not allowed, and trailing spaces and trailing dots cause problems too. A small function that replaces these characters with an underscore and strips extra spaces prevents errors that are hard to diagnose.

Situation Risk Countermeasure
Identical names Overwrite or error exists() check
Overlapping rules Chain conflicts Two-pass rename
Forbidden characters Error on Windows Cleaning function
Mistake in the rule Wrong names in bulk Dry run and log

From test to recurring job

Once tested, the script can take the folder as a command-line argument, so you can use it on different archives without editing it. The next step is to schedule it, for example every evening on the folder of incoming scans. Even then the dry run remains useful: just run it every time you change the rule.

In short

  • pathlib makes batch renaming simple and readable, in just a few lines of code.
  • Always run a dry run first that prints the transformations without applying them.
  • Use dates in YYYY-MM-DD format and zero-padded numbers for correct sorting.
  • Handle name conflicts and forbidden characters explicitly.
  • Log every change in a CSV so you can undo it.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also