How to Safely Bulk Rename Files with Python

Python can rename many files consistently, but the safe part is the planning and rollback—not the rename() call itself. The script below builds a complete plan, detects duplicate and pre-existing target names before touching a file, writes a CSV manifest, requires an exact confirmation, and uses temporary names so source and destination names can overlap.

Use copies first: run any bulk rename on a small duplicate folder before using it on irreplaceable files. A power loss, permission failure, cloud-sync conflict, or incorrect sort order can still interrupt a well-designed script.

Decide the naming rule before writing code

Write down the exact source folder, sort order, prefix, starting number, number width, and extension behavior. A script cannot know which photograph should receive a business-specific identifier; it can only apply the rule you define.

Save the script outside the target folder

Create safe_renamer.py somewhere outside the folder being renamed. Replace the example path and prefix:

from pathlib import Path
from uuid import uuid4
import csv

folder = Path(r"C:\Users\YourName\Desktop\ProductPhotos")
prefix = "SUMMER_COLLECTION"
manifest_path = folder / "rename-manifest.csv"

if not folder.is_dir():
    raise SystemExit(f"Folder not found: {folder}")

files = sorted(
    (
        item for item in folder.iterdir()
        if item.is_file()
        and item.name != manifest_path.name
        and not item.name.startswith(".__rename_stage_")
    ),
    key=lambda item: item.name.casefold()
)

if not files:
    raise SystemExit("No files were found in the selected folder.")

plan = []
for number, old_path in enumerate(files, start=1):
    new_name = f"{prefix}_{number:03d}{old_path.suffix}"
    plan.append((old_path, folder / new_name))

# Case-insensitive checks also catch common Windows collisions.
target_keys = [str(target).casefold() for _, target in plan]
if len(target_keys) != len(set(target_keys)):
    raise SystemExit("Two source files would receive the same target name.")

source_keys = {str(source.resolve()).casefold() for source, _ in plan}
conflicts = []
for _, target in plan:
    if target.exists() and str(target.resolve()).casefold() not in source_keys:
        conflicts.append(target.name)

if conflicts:
    raise SystemExit(
        "Unrelated target files already exist: " + ", ".join(conflicts)
    )

if manifest_path.exists():
    raise SystemExit(
        f"Manifest already exists: {manifest_path}. Move or review it first."
    )

with manifest_path.open("w", newline="", encoding="utf-8-sig") as handle:
    writer = csv.writer(handle)
    writer.writerow(["OriginalName", "NewName"])
    writer.writerows((source.name, target.name) for source, target in plan)

print("Planned changes:")
for source, target in plan:
    print(f"{source.name}  ->  {target.name}")

answer = input("Type RENAME to apply this exact plan: ")
if answer != "RENAME":
    raise SystemExit(f"No files changed. Review: {manifest_path}")

staged = []
completed = []

try:
    for source, target in plan:
        temp = folder / f".__rename_stage_{uuid4().hex}{source.suffix}"
        source.rename(temp)
        staged.append((source, temp, target))

    for source, temp, target in staged:
        temp.rename(target)
        completed.append((source, target))

except Exception:
    # Best-effort rollback. Keep the manifest for manual recovery.
    for source, target in reversed(completed):
        if target.exists() and not source.exists():
            target.rename(source)

    for source, temp, _ in reversed(staged):
        if temp.exists() and not source.exists():
            temp.rename(source)

    raise

print(f"Renamed {len(completed)} files.")
print(f"Manifest: {manifest_path}")

Preview what the script will include

The script processes files directly inside the selected folder and ignores subfolders. It also excludes its own manifest and temporary staging names. Hidden files are still files, so inspect the printed plan and remove any item that should not be renamed.

Understand extension handling

old_path.suffix preserves only the final suffix. For example, archive.tar.gz is treated as having the suffix .gz. If compound extensions matter, define a separate rule instead of assuming every dot belongs to the extension.

Why the preflight checks matter

Run a controlled test

  1. Create a temporary folder containing copies of several real file types.
  2. Include names that already resemble the target pattern.
  3. Run the script and stop at the preview the first time.
  4. Open the CSV manifest and confirm the sort order.
  5. Run again, type RENAME, and open several renamed files.
  6. Confirm that the expected count, extensions, and file contents are unchanged.

Recover from an interrupted run

If the script raises an exception, it attempts a best-effort rollback. A computer crash can still leave staging names beginning with .__rename_stage_. Do not delete them. Use the manifest and a backup to restore the mapping. If the folder is synchronized by OneDrive, Google Drive, or another service, pause and review sync activity before retrying.

When not to use sequential renaming

Do not use this approach when filenames contain required case numbers, legal identifiers, dates, or references used by another application unless you have verified every dependency. A database, digital-asset manager, or application-specific export may be the correct place to change those names.

Completion checklist

Official Python reference

Related Guides

About the author

Tweaknook Editorial publishes practical guides and browser-based tools for everyday digital work. Product-dependent facts are checked against current primary documentation, with limitations and safer verification steps stated where relevant.