Batch Rename Hundreds of Files in Seconds with Python
You've got 500 photos from an event named IMG_0001.jpg through IMG_0500.jpg. You need them named company_retreat_2025_001.jpg through company_retreat_2025_500.jpg.
Renaming them one by one? That's 8+ hours of mind-numbing work. A Python script? About 2 seconds.
Let's build a flexible file renaming tool that handles patterns, sequences, date insertion, and more.
What You'll Learn
- How to safely rename files with Python
- Building flexible naming patterns
- Working with regular expressions for advanced renaming
- Creating dry-run modes to preview changes safely
Prerequisites
- Python 3.8 or higher
- No external libraries needed
- A folder of files you want to rename
The Problem
File renaming is tedious when you need to:
- Add prefixes or suffixes to many files
- Replace text patterns across filenames
- Add sequential numbers
- Standardize naming conventions
- Insert dates or other metadata
File Explorer and Finder have basic batch rename features, but they're limited. Python gives you complete control.
The Solution
We'll build a script that supports multiple renaming modes:
- Pattern replacement: Replace text in filenames
- Prefix/Suffix: Add text to the beginning or end
- Sequential numbering: Rename with numbered sequences
- Case conversion: Standardize to lowercase, uppercase, or title case
Step 1: Setting Up Safe Renaming
The key to safe renaming is never losing files. We'll always check for conflicts and provide a preview mode:
1from pathlib import Path234def get_files_in_folder(folder_path, extension_filter=None):5 """6 Get all files in a folder, optionally filtered by extension.78 Args:9 folder_path: Path to the folder10 extension_filter: Optional extension like '.jpg' or list ['.jpg', '.png']1112 Returns:13 List of Path objects for matching files14 """15 folder = Path(folder_path)1617 if not folder.exists():18 raise FileNotFoundError(f"Folder not found: {folder_path}")1920 files = [f for f in folder.iterdir() if f.is_file()]2122 # Filter by extension if specified23 if extension_filter:24 if isinstance(extension_filter, str):25 extension_filter = [extension_filter]2627 # Normalize extensions to lowercase with leading dot28 extensions = [ext.lower() if ext.startswith('.') else f'.{ext.lower()}'29 for ext in extension_filter]3031 files = [f for f in files if f.suffix.lower() in extensions]3233 return sorted(files)
Step 2: Pattern Replacement
The most common renaming need—find and replace text in filenames:
1def rename_replace_pattern(files, find_text, replace_text, case_sensitive=True):2 """3 Replace a pattern in all filenames.45 Args:6 files: List of Path objects7 find_text: Text to find8 replace_text: Text to replace with9 case_sensitive: Whether to match case1011 Returns:12 List of tuples: (original_path, new_name)13 """14 renames = []1516 for file_path in files:17 original_name = file_path.stem # Filename without extension18 extension = file_path.suffix1920 if case_sensitive:21 new_name = original_name.replace(find_text, replace_text)22 else:23 # Case-insensitive replacement24 import re25 pattern = re.compile(re.escape(find_text), re.IGNORECASE)26 new_name = pattern.sub(replace_text, original_name)2728 # Only include if name actually changed29 if new_name != original_name:30 renames.append((file_path, f"{new_name}{extension}"))3132 return renames
Step 3: Sequential Numbering
Rename files with a pattern and sequential numbers:
1def rename_sequential(files, pattern, start_number=1, padding=3):2 """3 Rename files with sequential numbers.45 Args:6 files: List of Path objects7 pattern: Name pattern with {n} for number placeholder8 Example: "photo_{n}" -> "photo_001.jpg"9 start_number: Starting number for sequence10 padding: Number of digits (3 means 001, 002, etc.)1112 Returns:13 List of tuples: (original_path, new_name)14 """15 renames = []1617 for i, file_path in enumerate(files):18 extension = file_path.suffix19 number = start_number + i2021 # Format number with padding22 formatted_number = str(number).zfill(padding)2324 # Replace placeholder with number25 new_name = pattern.replace("{n}", formatted_number)2627 renames.append((file_path, f"{new_name}{extension}"))2829 return renames
Step 4: Prefix and Suffix Operations
Add text to the beginning or end of filenames:
1def rename_add_prefix(files, prefix):2 """Add a prefix to all filenames."""3 renames = []45 for file_path in files:6 new_name = f"{prefix}{file_path.name}"7 renames.append((file_path, new_name))89 return renames101112def rename_add_suffix(files, suffix):13 """Add a suffix before the extension."""14 renames = []1516 for file_path in files:17 new_name = f"{file_path.stem}{suffix}{file_path.suffix}"18 renames.append((file_path, new_name))1920 return renames
Step 5: Safe Execution with Preview
Before making changes, we should preview them and check for conflicts:
1def check_conflicts(renames, folder_path):2 """3 Check for naming conflicts.45 Args:6 renames: List of (original_path, new_name) tuples7 folder_path: Folder where files are located89 Returns:10 List of conflict descriptions11 """12 folder = Path(folder_path)13 conflicts = []1415 # Track new names to detect duplicates within our rename set16 new_names = {}1718 for original_path, new_name in renames:19 # Check if new name already exists in folder20 new_path = folder / new_name21 if new_path.exists() and new_path != original_path:22 conflicts.append(f"'{new_name}' already exists in folder")2324 # Check for duplicates in our rename list25 if new_name in new_names:26 conflicts.append(f"Duplicate new name: '{new_name}'")27 new_names[new_name] = original_path2829 return conflicts303132def execute_renames(renames, dry_run=True):33 """34 Execute the rename operations.3536 Args:37 renames: List of (original_path, new_name) tuples38 dry_run: If True, only preview changes without renaming3940 Returns:41 Number of files renamed42 """43 if dry_run:44 print("\n📋 PREVIEW MODE (no changes will be made)")45 print("=" * 60)4647 for original_path, new_name in renames:48 print(f" {original_path.name}")49 print(f" → {new_name}")50 print()5152 print(f"Total: {len(renames)} file(s) would be renamed")53 return 05455 # Execute renames56 print("\n🔄 RENAMING FILES")57 print("=" * 60)5859 renamed_count = 060 for original_path, new_name in renames:61 new_path = original_path.parent / new_name6263 try:64 original_path.rename(new_path)65 print(f" ✓ {original_path.name} → {new_name}")66 renamed_count += 167 except Exception as e:68 print(f" ✗ {original_path.name}: {e}")6970 print(f"\n✅ Renamed {renamed_count} of {len(renames)} files")71 return renamed_count
The Complete Script
1#!/usr/bin/env python32"""3Batch File Renamer - Rename multiple files with patterns and sequences.4Author: Alex Rodriguez56Supports: pattern replacement, sequential numbering, prefix/suffix,7and case conversion with dry-run preview mode.8"""910import re11from pathlib import Path121314def get_files_in_folder(folder_path, extension_filter=None):15 """16 Get all files in a folder, optionally filtered by extension.1718 Args:19 folder_path: Path to the folder20 extension_filter: Optional extension like '.jpg' or list ['.jpg', '.png']2122 Returns:23 List of Path objects for matching files24 """25 folder = Path(folder_path)2627 if not folder.exists():28 raise FileNotFoundError(f"Folder not found: {folder_path}")2930 files = [f for f in folder.iterdir() if f.is_file()]3132 if extension_filter:33 if isinstance(extension_filter, str):34 extension_filter = [extension_filter]3536 extensions = [ext.lower() if ext.startswith('.') else f'.{ext.lower()}'37 for ext in extension_filter]3839 files = [f for f in files if f.suffix.lower() in extensions]4041 return sorted(files)424344def rename_replace_pattern(files, find_text, replace_text, case_sensitive=True):45 """Replace a pattern in all filenames."""46 renames = []4748 for file_path in files:49 original_name = file_path.stem50 extension = file_path.suffix5152 if case_sensitive:53 new_name = original_name.replace(find_text, replace_text)54 else:55 pattern = re.compile(re.escape(find_text), re.IGNORECASE)56 new_name = pattern.sub(replace_text, original_name)5758 if new_name != original_name:59 renames.append((file_path, f"{new_name}{extension}"))6061 return renames626364def rename_sequential(files, pattern, start_number=1, padding=3):65 """66 Rename files with sequential numbers.6768 Pattern uses {n} as placeholder for the number.69 Example: "photo_{n}" with padding=3 -> "photo_001.jpg"70 """71 renames = []7273 for i, file_path in enumerate(files):74 extension = file_path.suffix75 number = start_number + i76 formatted_number = str(number).zfill(padding)77 new_name = pattern.replace("{n}", formatted_number)78 renames.append((file_path, f"{new_name}{extension}"))7980 return renames818283def rename_add_prefix(files, prefix):84 """Add a prefix to all filenames."""85 renames = []86 for file_path in files:87 new_name = f"{prefix}{file_path.name}"88 renames.append((file_path, new_name))89 return renames909192def rename_add_suffix(files, suffix):93 """Add a suffix before the extension."""94 renames = []95 for file_path in files:96 new_name = f"{file_path.stem}{suffix}{file_path.suffix}"97 renames.append((file_path, new_name))98 return renames99100101def rename_change_case(files, case_type):102 """103 Change filename case.104105 Args:106 files: List of Path objects107 case_type: 'lower', 'upper', or 'title'108 """109 renames = []110111 for file_path in files:112 original_name = file_path.stem113 extension = file_path.suffix.lower() # Extensions usually lowercase114115 if case_type == 'lower':116 new_name = original_name.lower()117 elif case_type == 'upper':118 new_name = original_name.upper()119 elif case_type == 'title':120 new_name = original_name.title()121 else:122 continue123124 if new_name != original_name or extension != file_path.suffix:125 renames.append((file_path, f"{new_name}{extension}"))126127 return renames128129130def rename_clean_filename(files):131 """132 Clean filenames: remove special chars, replace spaces with underscores.133 """134 renames = []135136 for file_path in files:137 original_name = file_path.stem138 extension = file_path.suffix.lower()139140 # Replace spaces with underscores141 new_name = original_name.replace(' ', '_')142143 # Remove special characters (keep letters, numbers, underscores, hyphens)144 new_name = re.sub(r'[^\w\-]', '', new_name)145146 # Remove multiple consecutive underscores147 new_name = re.sub(r'_+', '_', new_name)148149 # Remove leading/trailing underscores150 new_name = new_name.strip('_')151152 if new_name != original_name or extension != file_path.suffix:153 renames.append((file_path, f"{new_name}{extension}"))154155 return renames156157158def check_conflicts(renames, folder_path):159 """Check for naming conflicts."""160 folder = Path(folder_path)161 conflicts = []162 new_names = {}163164 for original_path, new_name in renames:165 new_path = folder / new_name166 if new_path.exists() and new_path != original_path:167 conflicts.append(f"'{new_name}' already exists in folder")168169 if new_name in new_names:170 conflicts.append(f"Duplicate new name: '{new_name}'")171 new_names[new_name] = original_path172173 return conflicts174175176def execute_renames(renames, dry_run=True):177 """Execute the rename operations."""178 if not renames:179 print("\n⚠️ No files to rename")180 return 0181182 if dry_run:183 print("\n📋 PREVIEW MODE (no changes will be made)")184 print("=" * 60)185186 for original_path, new_name in renames:187 print(f" {original_path.name}")188 print(f" → {new_name}")189 print()190191 print(f"Total: {len(renames)} file(s) would be renamed")192 print("\n💡 Run with dry_run=False to execute these changes")193 return 0194195 print("\n🔄 RENAMING FILES")196 print("=" * 60)197198 renamed_count = 0199 for original_path, new_name in renames:200 new_path = original_path.parent / new_name201202 try:203 original_path.rename(new_path)204 print(f" ✓ {original_path.name} → {new_name}")205 renamed_count += 1206 except Exception as e:207 print(f" ✗ {original_path.name}: {e}")208209 print(f"\n✅ Renamed {renamed_count} of {len(renames)} files")210 return renamed_count211212213def main():214 """Main entry point with example usage."""215216 # ========================================217 # CONFIGURE YOUR RENAMING OPERATION HERE218 # ========================================219220 # Folder containing files to rename221 folder_path = "/path/to/your/files"222223 # Filter by extension (optional) - set to None for all files224 extension_filter = [".jpg", ".jpeg", ".png"]225226 # ========================================227 # CHOOSE YOUR RENAMING MODE228 # Uncomment ONE of the sections below229 # ========================================230231 # Get files232 try:233 files = get_files_in_folder(folder_path, extension_filter)234 print(f"Found {len(files)} files in {folder_path}")235 except FileNotFoundError as e:236 print(f"Error: {e}")237 return238239 if not files:240 print("No matching files found!")241 return242243 # --- MODE 1: Replace pattern in filenames ---244 # renames = rename_replace_pattern(245 # files,246 # find_text="IMG_",247 # replace_text="vacation_",248 # case_sensitive=False249 # )250251 # --- MODE 2: Sequential numbering ---252 renames = rename_sequential(253 files,254 pattern="company_retreat_2025_{n}",255 start_number=1,256 padding=3257 )258259 # --- MODE 3: Add prefix ---260 # renames = rename_add_prefix(files, prefix="2025_")261262 # --- MODE 4: Add suffix ---263 # renames = rename_add_suffix(files, suffix="_final")264265 # --- MODE 5: Change case ---266 # renames = rename_change_case(files, case_type='lower')267268 # --- MODE 6: Clean filenames ---269 # renames = rename_clean_filename(files)270271 # ========================================272 # CHECK FOR CONFLICTS273 # ========================================274275 conflicts = check_conflicts(renames, folder_path)276 if conflicts:277 print("\n❌ CONFLICTS DETECTED:")278 for conflict in conflicts:279 print(f" • {conflict}")280 print("\nResolve conflicts before proceeding.")281 return282283 # ========================================284 # EXECUTE (set dry_run=False to apply changes)285 # ========================================286287 execute_renames(renames, dry_run=True)288289290if __name__ == "__main__":291 main()
How to Run This Script
-
Save the script as
batch_renamer.py -
Edit the configuration in the
main()function:- Set
folder_pathto your folder - Choose your extension filter
- Uncomment the renaming mode you want
- Set
-
Run in preview mode first:
bash1python batch_renamer.py -
Review the preview output:
PromptFound 50 files in /Users/yourname/Photos 📋 PREVIEW MODE (no changes will be made) ============================================================ IMG_0001.jpg → company_retreat_2025_001.jpg IMG_0002.jpg → company_retreat_2025_002.jpg Total: 50 file(s) would be renamed 💡 Run with dry_run=False to execute these changes -
Execute the renames by changing
dry_run=Truetodry_run=False
Customization Options
Rename Using File Date
Add creation date to filenames:
1from datetime import datetime23def rename_with_date(files, pattern):4 """Add file modification date to filename."""5 renames = []67 for file_path in files:8 mod_time = file_path.stat().st_mtime9 date_str = datetime.fromtimestamp(mod_time).strftime("%Y%m%d")1011 new_name = pattern.replace("{date}", date_str)12 new_name = f"{new_name}{file_path.suffix}"1314 renames.append((file_path, new_name))1516 return renames1718# Usage: rename_with_date(files, "photo_{date}")19# Result: photo_20251104.jpg
Remove Numbers from Filenames
1def rename_remove_numbers(files):2 """Remove all numbers from filenames."""3 renames = []45 for file_path in files:6 new_name = re.sub(r'\d+', '', file_path.stem)7 new_name = new_name.strip('_- ') # Clean up leftover separators89 if new_name: # Don't create empty names10 renames.append((file_path, f"{new_name}{file_path.suffix}"))1112 return renames
Regex Pattern Matching
For complex patterns:
1def rename_regex(files, regex_pattern, replacement):2 """Use regex for advanced pattern matching."""3 renames = []4 pattern = re.compile(regex_pattern)56 for file_path in files:7 new_name = pattern.sub(replacement, file_path.stem)89 if new_name != file_path.stem:10 renames.append((file_path, f"{new_name}{file_path.suffix}"))1112 return renames1314# Example: Remove timestamps like "_20251104_143022"15# rename_regex(files, r'_\d{8}_\d{6}', '')
Common Issues & Solutions
| Issue | Solution |
|---|---|
| "File not found" error | Double-check folder_path is correct and exists |
| No files found | Check extension_filter matches your file types |
| Permission denied | Close files that are open in other programs |
| Duplicate conflict | The new name already exists; adjust your pattern |
| Files out of order | Files are sorted alphabetically; use padding for numbers |
Taking It Further
Create an Undo File
Save original names for reverting:
1import json23def save_undo_file(renames, undo_path="rename_undo.json"):4 """Save rename mapping for undo capability."""5 undo_data = [6 {"original": str(orig), "new": new_name}7 for orig, new_name in renames8 ]910 with open(undo_path, 'w') as f:11 json.dump(undo_data, f, indent=2)1213 print(f"Undo file saved: {undo_path}")
Add Command-Line Arguments
Make the script more flexible:
1import argparse23parser = argparse.ArgumentParser(description='Batch rename files')4parser.add_argument('folder', help='Folder containing files')5parser.add_argument('--find', help='Text to find')6parser.add_argument('--replace', help='Text to replace with')7parser.add_argument('--prefix', help='Prefix to add')8parser.add_argument('--execute', action='store_true', help='Execute renames')910args = parser.parse_args()
Conclusion
You've built a powerful, safe batch file renamer. The dry-run mode means you'll never accidentally destroy your files—you can preview every change before it happens.
The modular design lets you mix and match operations. Need to clean filenames AND add a prefix? Chain the functions together. Want to add new renaming modes? Just add another function following the same pattern.
File renaming is one of those tasks that seems simple until you have 500 files to process. Now you have a tool that handles any scenario in seconds.
Your filenames, your rules.
Sponsored Content
Interested in advertising? Reach automation professionals through our platform.
