Python rename files from a CSV list
Last updated .
If the old and new names are already in a spreadsheet, Python can apply the list without a rename framework.
This page gives a complete script that runs on macOS, Linux and Windows, previews by default, and refuses to
overwrite. Copy it into rename_from_csv.py and run it with Python 3; there is nothing to install.
The CSV the script expects
Two columns, old and new, with a header row. Save the sheet as CSV UTF-8. The old
name is the file as it exists in the folder; the new name is the name you want. A matching extension in the
new column is ignored, because the extension of the file on disk always wins.
old,new
IMG_0001.jpg,SKU-1001
IMG_0002.jpg,SKU-1002
with space.txt,report final.txt
The script
It reads the whole mapping first, validates every row, then renames through a temporary folder. That last
part matters when rows depend on each other: a→b and b→c in the same batch would
lose a file if they ran top to bottom.
#!/usr/bin/env python3
"""Rename files from a two-column CSV. Dry run by default; pass --apply to rename."""
import csv
import shutil
import sys
from pathlib import Path
BAD = set('<>:"/\\|?*')
def load_plan(csv_path: Path, folder: Path):
with csv_path.open(newline="", encoding="utf-8-sig") as fh:
rows = list(csv.reader(fh))
if rows and [c.strip().lower() for c in rows[0][:2]] == ["old", "new"]:
rows = rows[1:]
plan, errors, seen = [], [], set()
for line, row in enumerate(rows, start=2):
if not row or not any(cell.strip() for cell in row):
continue
if len(row) < 2:
errors.append(f"line {line}: needs two columns (old,new)")
continue
old, new = row[0].strip(), row[1].strip()
if not old or not new:
errors.append(f"line {line}: old and new names are both required")
continue
if old != Path(old).name or "\\" in old:
errors.append(f"line {line}: old name must be a file in the folder, not a path")
continue
src = folder / old
if not src.is_file():
errors.append(f"line {line}: no file named {old!r}")
continue
if any(ch in BAD for ch in new) or new.endswith((" ", ".")):
errors.append(f"line {line}: {new!r} is not a safe file name")
continue
base = new[:-len(src.suffix)] if src.suffix and new.lower().endswith(src.suffix.lower()) else new
target = src.with_name(base + src.suffix)
if target.name == ".rename-tmp":
errors.append(f"line {line}: .rename-tmp is reserved for the temporary folder")
continue
key = target.name.casefold()
if key in seen:
errors.append(f"line {line}: two rows produce the same name {target.name!r}")
continue
seen.add(key)
plan.append((src, target))
return plan, errors
def main():
if len(sys.argv) not in (3, 4):
sys.exit("usage: python rename_from_csv.py mapping.csv folder [--apply]")
mapping, folder = Path(sys.argv[1]), Path(sys.argv[2])
apply = "--apply" in sys.argv[3:]
plan, errors = load_plan(mapping, folder)
if errors:
print("\n".join(errors), file=sys.stderr)
sys.exit(f"stopped: {len(errors)} problem(s) found; nothing was renamed")
sources = {src for src, _ in plan}
for _, target in plan:
if target.exists() and target not in sources:
sys.exit(f"stopped: {target.name!r} already exists; nothing was renamed")
for src, target in plan:
print(f"{src.name} -> {target.name}")
if not apply:
print(f"\nDry run: {len(plan)} file(s) would be renamed. Re-run with --apply.")
return
temp = folder / ".rename-tmp"
if temp.exists():
sys.exit(f"stopped: {temp} exists; remove it after checking for leftovers")
temp.mkdir()
moved = []
try:
for i, (src, target) in enumerate(plan):
tmp = temp / f"{i:06d}{src.suffix}"
src.rename(tmp)
moved.append((tmp, target, src))
for tmp, target, _ in moved:
tmp.rename(target)
except OSError as exc:
for tmp, target, src in reversed(moved):
try:
if tmp.exists():
tmp.rename(src)
elif target.exists() and not src.exists():
target.rename(src)
except OSError:
pass
shutil.rmtree(temp, ignore_errors=True)
sys.exit(f"stopped: {exc}; attempted to put every file back")
temp.rmdir()
print(f"\nRenamed {len(plan)} file(s).")
if __name__ == "__main__":
main()
Run it
Put the script next to the folder, or anywhere and pass the paths. Preview first:
python3 rename_from_csv.py names.csv ./photos
If every pair looks right, run the same command with --apply:
python3 rename_from_csv.py names.csv ./photos --apply
On Windows the command is python rename_from_csv.py names.csv .\photos. If Python is not
installed, use the PowerShell guide or the browser tool instead.
What it refuses to do
- Rename a row whose old name does not exist in the folder.
- Let two rows produce the same target name, including names that differ only by case.
- Overwrite a file that is not part of the mapping.
- Write a separator, an illegal character, or a trailing dot or space into a name.
- Take the extension from the spreadsheet instead of the file on disk.
When a script is the wrong tool
A script is fine when you are comfortable editing one and running it again next month. If you would rather not, Mass Rename does the same lookup in a browser tab: drop the CSV and the files, check the preview, download the renamed ZIP. The files are never uploaded, and the free tier covers 50 files per batch. The PowerShell and bash guides cover the same job in other shells.
Questions about renaming with Python
Does the script delete or overwrite files?
No. It checks every row before touching anything, refuses a batch with a missing source or a duplicate target, and moves files through a temporary folder so swaps and chains do not clobber each other. If a step fails it tries to put every file back.
Does it run on Windows?
Yes, with Python 3 installed. The script uses only the standard library, and pathlib handles Windows and macOS paths. Run it from Command Prompt or PowerShell with python rename_from_csv.py.
What should the CSV look like?
Two columns, old and new, with a header row. Save it as CSV UTF-8. A matching file extension in the new name is ignored: the extension is taken from the file on disk.
How do I preview before renaming?
Run the script without --apply. It prints every old to new pair and exits without touching the files. Add --apply only when the list looks right.
Why does it use a temporary folder?
Because renames can depend on each other. If one row turns a into b while another turns b into c, renaming top to bottom would destroy the file the second row still needs. Moving everything aside first makes the order irrelevant.
What if two rows ask for the same name?
The script stops before renaming anything and names the duplicate. Fix the spreadsheet and run it again; it will not guess which file should win.
Can I use this instead of installing Mass Rename?
Yes. If you would rather not run a script, Mass Rename does the same lookup in a browser tab: drop the CSV and the files, check the preview, download the renamed ZIP. Files are never uploaded.