Creating a CLI Utility for Bulk File Rename Operations Using Python
Renaming hundreds of files manually is tedious and error-prone. A Python command-line utility can automate bulk renaming with patterns, regex substitution, numbering sequences, and dry-run previews. This article walks through building a practical CLI tool using argparse and pathlib, covering common renaming scenarios like normalizing filenames, adding prefixes/suffixes, replacing text, and numbering files sequentially.
Core Design with argparse and pathlib
Python’s argparse module handles command-line argument parsing, and pathlib provides an object-oriented interface to filesystem paths. The tool should support several rename modes: replace (find and replace text in filenames), prefix/suffix (add leading or trailing text), number (add sequential numbering), and regex (pattern-based replacement using regular expressions). A dry-run flag (-n or –dry-run) is essential—it shows what would happen without actually renaming anything, letting users verify the operation before executing.
import argparse, re
from pathlib import Path
def bulk_rename(directory, find=None, replace=None,
prefix="", suffix="", dry_run=False):
path = Path(directory)
for file in path.iterdir():
if not file.is_file():
continue
old_name = file.name
new_name = old_name
if find and replace is not None:
new_name = new_name.replace(find, replace)
if prefix:
new_name = prefix + new_name
if suffix:
stem = Path(new_name).stem
ext = Path(new_name).suffix
new_name = f"{stem}{suffix}{ext}"
if new_name != old_name:
print(f" {old_name} → {new_name}")
if not dry_run:
file.rename(file.with_name(new_name))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Bulk rename files")
parser.add_argument("directory", help="Target directory")
parser.add_argument("--find", help="Text to find")
parser.add_argument("--replace", help="Replacement text")
parser.add_argument("--prefix", default="", help="Add prefix")
parser.add_argument("--suffix", default="", help="Add suffix")
parser.add_argument("-n", "--dry-run", action="store_true", help="Preview only")
args = parser.parse_args()
bulk_rename(args.directory, args.find, args.replace,
args.prefix, args.suffix, args.dry_run)
Sequential Numbering
Adding sequential numbers to files is useful for photo collections, document scanning, or creating ordered playlists. The –number flag adds a zero-padded sequence number (e.g., 001, 002) to each file. You can specify the starting number, padding width, and position (prefix vs suffix). Sorting can be by name, modification date, or creation date to control the numbering order. The script detects and prevents collisions by checking whether the target filename already exists before renaming.
def add_numbering(files, start=1, padding=3, as_prefix=True, by="name"):
if by == "date":
files.sort(key=lambda f: f.stat().st_mtime)
else:
files.sort()
for i, file in enumerate(files, start=start):
num = str(i).zfill(padding)
old = file.name
stem = file.stem
ext = file.suffix
new_name = f"{num}_{stem}{ext}" if as_prefix else f"{stem}_{num}{ext}"
yield old, new_name
# Usage: python rename.py ./photos --number --start 1 --padding 4 --by date
Regex-Based Renaming
For complex transformations, regex is indispensable. The –regex flag enables pattern-based matching with capture groups that can be referenced in the replacement string (e.g., , ). This is useful for extracting and reformatting date patterns, normalizing spacing, or restructuring naming conventions. For example, renaming “IMG_20260709_123456.jpg” to “2026-07-09_12-34-56.jpg” uses a single regex substitution with capture groups for year, month, day, hour, minute, and second.
def regex_rename(directory, pattern, replacement, dry_run=False):
path = Path(directory)
for file in path.iterdir():
if not file.is_file():
continue
new_name = re.sub(pattern, replacement, file.name)
if new_name != file.name:
print(f" {file.name} → {new_name}")
if not dry_run:
file.rename(file.with_name(new_name))
# Example: python rename.py ./photos --regex "(IMG_)(\d{4})(\d{2})(\d{2})" --replace "--_"
Safety Features
Beyond dry-run mode, the utility should include collision detection (preventing overwrites), undo functionality (saving rename operations to a log file that can reverse them), and confirmation prompts before executing on more than a threshold number of files. Using pathlib’s rename() method is atomic on most filesystems, meaning a partially completed batch leaves some files renamed and others not—logging each operation to a JSON file allows reversing with a simple –undo flag that reads the log and reverses the mapping.
Cross-Platform Considerations
Python’s pathlib.Path handles path separators correctly on Windows (backslash), macOS, and Linux. However, renaming files across filesystems (e.g., renaming on an external drive) may not be atomic. The script should handle permission errors gracefully by catching PermissionError and continuing with the remaining files. On Unix systems, renaming a file to a name that differs only in case may behave unexpectedly on case-insensitive filesystems (macOS default, Windows). Adding a warning when –find and –replace would change only case prevents silent failures. For very large directories (100K+ files), using os.scandir() instead of pathlib.iterdir() improves initial listing speed, and batching rename operations in transactions of 1000 files prevents partial failures from leaving the directory in an inconsistent state.
GUI Frontend with Tkinter or PyQt
For users uncomfortable with the command line, a simple GUI frontend provides the same functionality with file dialogs and preview lists. Python’s tkinter (built-in) creates native-looking dialogs for selecting directories, defining rename rules, and previewing changes before applying them. A GUI version shows the original filenames next to the new names with color coding (green = rename, red = conflict, gray = unchanged). Drag-and-drop support lets users drop files or folders onto the window. The PyQt6 version includes a progress bar for large directories, a parallel rename option, and an undo button that reverses the last rename operation.
