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 Path
2
3
4def get_files_in_folder(folder_path, extension_filter=None):
5 """
6 Get all files in a folder, optionally filtered by extension.
7
8 Args:
9 folder_path: Path to the folder
10 extension_filter: Optional extension like '.jpg' or list ['.jpg', '.png']
11
12 Returns:
13 List of Path objects for matching files
14 """
15 folder = Path(folder_path)
16
17 if not folder.exists():
18 raise FileNotFoundError(f"Folder not found: {folder_path}")
19
20 files = [f for f in folder.iterdir() if f.is_file()]
21
22 # Filter by extension if specified
23 if extension_filter:
24 if isinstance(extension_filter, str):
25 extension_filter = [extension_filter]
26
27 # Normalize extensions to lowercase with leading dot
28 extensions = [ext.lower() if ext.startswith('.') else f'.{ext.lower()}'
29 for ext in extension_filter]
30
31 files = [f for f in files if f.suffix.lower() in extensions]
32
33 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.
4
5 Args:
6 files: List of Path objects
7 find_text: Text to find
8 replace_text: Text to replace with
9 case_sensitive: Whether to match case
10
11 Returns:
12 List of tuples: (original_path, new_name)
13 """
14 renames = []
15
16 for file_path in files:
17 original_name = file_path.stem # Filename without extension
18 extension = file_path.suffix
19
20 if case_sensitive:
21 new_name = original_name.replace(find_text, replace_text)
22 else:
23 # Case-insensitive replacement
24 import re
25 pattern = re.compile(re.escape(find_text), re.IGNORECASE)
26 new_name = pattern.sub(replace_text, original_name)
27
28 # Only include if name actually changed
29 if new_name != original_name:
30 renames.append((file_path, f"{new_name}{extension}"))
31
32 return renamesStep 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.
4
5 Args:
6 files: List of Path objects
7 pattern: Name pattern with {n} for number placeholder
8 Example: "photo_{n}" -> "photo_001.jpg"
9 start_number: Starting number for sequence
10 padding: Number of digits (3 means 001, 002, etc.)
11
12 Returns:
13 List of tuples: (original_path, new_name)
14 """
15 renames = []
16
17 for i, file_path in enumerate(files):
18 extension = file_path.suffix
19 number = start_number + i
20
21 # Format number with padding
22 formatted_number = str(number).zfill(padding)
23
24 # Replace placeholder with number
25 new_name = pattern.replace("{n}", formatted_number)
26
27 renames.append((file_path, f"{new_name}{extension}"))
28
29 return renamesStep 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 = []
4
5 for file_path in files:
6 new_name = f"{prefix}{file_path.name}"
7 renames.append((file_path, new_name))
8
9 return renames
10
11
12def rename_add_suffix(files, suffix):
13 """Add a suffix before the extension."""
14 renames = []
15
16 for file_path in files:
17 new_name = f"{file_path.stem}{suffix}{file_path.suffix}"
18 renames.append((file_path, new_name))
19
20 return renamesStep 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.
4
5 Args:
6 renames: List of (original_path, new_name) tuples
7 folder_path: Folder where files are located
8
9 Returns:
10 List of conflict descriptions
11 """
12 folder = Path(folder_path)
13 conflicts = []
14
15 # Track new names to detect duplicates within our rename set
16 new_names = {}
17
18 for original_path, new_name in renames:
19 # Check if new name already exists in folder
20 new_path = folder / new_name
21 if new_path.exists() and new_path != original_path:
22 conflicts.append(f"'{new_name}' already exists in folder")
23
24 # Check for duplicates in our rename list
25 if new_name in new_names:
26 conflicts.append(f"Duplicate new name: '{new_name}'")
27 new_names[new_name] = original_path
28
29 return conflicts
30
31
32def execute_renames(renames, dry_run=True):
33 """
34 Execute the rename operations.
35
36 Args:
37 renames: List of (original_path, new_name) tuples
38 dry_run: If True, only preview changes without renaming
39
40 Returns:
41 Number of files renamed
42 """
43 if dry_run:
44 print("\n📋 PREVIEW MODE (no changes will be made)")
45 print("=" * 60)
46
47 for original_path, new_name in renames:
48 print(f" {original_path.name}")
49 print(f" → {new_name}")
50 print()
51
52 print(f"Total: {len(renames)} file(s) would be renamed")
53 return 0
54
55 # Execute renames
56 print("\n🔄 RENAMING FILES")
57 print("=" * 60)
58
59 renamed_count = 0
60 for original_path, new_name in renames:
61 new_path = original_path.parent / new_name
62
63 try:
64 original_path.rename(new_path)
65 print(f" ✓ {original_path.name} → {new_name}")
66 renamed_count += 1
67 except Exception as e:
68 print(f" ✗ {original_path.name}: {e}")
69
70 print(f"\n✅ Renamed {renamed_count} of {len(renames)} files")
71 return renamed_countThe Complete Script
1#!/usr/bin/env python3
2"""
3Batch File Renamer - Rename multiple files with patterns and sequences.
4Author: Alex Rodriguez
5
6Supports: pattern replacement, sequential numbering, prefix/suffix,
7and case conversion with dry-run preview mode.
8"""
9
10import re
11from pathlib import Path
12
13
14def get_files_in_folder(folder_path, extension_filter=None):
15 """
16 Get all files in a folder, optionally filtered by extension.
17
18 Args:
19 folder_path: Path to the folder
20 extension_filter: Optional extension like '.jpg' or list ['.jpg', '.png']
21
22 Returns:
23 List of Path objects for matching files
24 """
25 folder = Path(folder_path)
26
27 if not folder.exists():
28 raise FileNotFoundError(f"Folder not found: {folder_path}")
29
30 files = [f for f in folder.iterdir() if f.is_file()]
31
32 if extension_filter:
33 if isinstance(extension_filter, str):
34 extension_filter = [extension_filter]
35
36 extensions = [ext.lower() if ext.startswith('.') else f'.{ext.lower()}'
37 for ext in extension_filter]
38
39 files = [f for f in files if f.suffix.lower() in extensions]
40
41 return sorted(files)
42
43
44def rename_replace_pattern(files, find_text, replace_text, case_sensitive=True):
45 """Replace a pattern in all filenames."""
46 renames = []
47
48 for file_path in files:
49 original_name = file_path.stem
50 extension = file_path.suffix
51
52 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)
57
58 if new_name != original_name:
59 renames.append((file_path, f"{new_name}{extension}"))
60
61 return renames
62
63
64def rename_sequential(files, pattern, start_number=1, padding=3):
65 """
66 Rename files with sequential numbers.
67
68 Pattern uses {n} as placeholder for the number.
69 Example: "photo_{n}" with padding=3 -> "photo_001.jpg"
70 """
71 renames = []
72
73 for i, file_path in enumerate(files):
74 extension = file_path.suffix
75 number = start_number + i
76 formatted_number = str(number).zfill(padding)
77 new_name = pattern.replace("{n}", formatted_number)
78 renames.append((file_path, f"{new_name}{extension}"))
79
80 return renames
81
82
83def 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 renames
90
91
92def 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 renames
99
100
101def rename_change_case(files, case_type):
102 """
103 Change filename case.
104
105 Args:
106 files: List of Path objects
107 case_type: 'lower', 'upper', or 'title'
108 """
109 renames = []
110
111 for file_path in files:
112 original_name = file_path.stem
113 extension = file_path.suffix.lower() # Extensions usually lowercase
114
115 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 continue
123
124 if new_name != original_name or extension != file_path.suffix:
125 renames.append((file_path, f"{new_name}{extension}"))
126
127 return renames
128
129
130def rename_clean_filename(files):
131 """
132 Clean filenames: remove special chars, replace spaces with underscores.
133 """
134 renames = []
135
136 for file_path in files:
137 original_name = file_path.stem
138 extension = file_path.suffix.lower()
139
140 # Replace spaces with underscores
141 new_name = original_name.replace(' ', '_')
142
143 # Remove special characters (keep letters, numbers, underscores, hyphens)
144 new_name = re.sub(r'[^\w\-]', '', new_name)
145
146 # Remove multiple consecutive underscores
147 new_name = re.sub(r'_+', '_', new_name)
148
149 # Remove leading/trailing underscores
150 new_name = new_name.strip('_')
151
152 if new_name != original_name or extension != file_path.suffix:
153 renames.append((file_path, f"{new_name}{extension}"))
154
155 return renames
156
157
158def check_conflicts(renames, folder_path):
159 """Check for naming conflicts."""
160 folder = Path(folder_path)
161 conflicts = []
162 new_names = {}
163
164 for original_path, new_name in renames:
165 new_path = folder / new_name
166 if new_path.exists() and new_path != original_path:
167 conflicts.append(f"'{new_name}' already exists in folder")
168
169 if new_name in new_names:
170 conflicts.append(f"Duplicate new name: '{new_name}'")
171 new_names[new_name] = original_path
172
173 return conflicts
174
175
176def execute_renames(renames, dry_run=True):
177 """Execute the rename operations."""
178 if not renames:
179 print("\n⚠️ No files to rename")
180 return 0
181
182 if dry_run:
183 print("\n📋 PREVIEW MODE (no changes will be made)")
184 print("=" * 60)
185
186 for original_path, new_name in renames:
187 print(f" {original_path.name}")
188 print(f" → {new_name}")
189 print()
190
191 print(f"Total: {len(renames)} file(s) would be renamed")
192 print("\n💡 Run with dry_run=False to execute these changes")
193 return 0
194
195 print("\n🔄 RENAMING FILES")
196 print("=" * 60)
197
198 renamed_count = 0
199 for original_path, new_name in renames:
200 new_path = original_path.parent / new_name
201
202 try:
203 original_path.rename(new_path)
204 print(f" ✓ {original_path.name} → {new_name}")
205 renamed_count += 1
206 except Exception as e:
207 print(f" ✗ {original_path.name}: {e}")
208
209 print(f"\n✅ Renamed {renamed_count} of {len(renames)} files")
210 return renamed_count
211
212
213def main():
214 """Main entry point with example usage."""
215
216 # ========================================
217 # CONFIGURE YOUR RENAMING OPERATION HERE
218 # ========================================
219
220 # Folder containing files to rename
221 folder_path = "/path/to/your/files"
222
223 # Filter by extension (optional) - set to None for all files
224 extension_filter = [".jpg", ".jpeg", ".png"]
225
226 # ========================================
227 # CHOOSE YOUR RENAMING MODE
228 # Uncomment ONE of the sections below
229 # ========================================
230
231 # Get files
232 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 return
238
239 if not files:
240 print("No matching files found!")
241 return
242
243 # --- MODE 1: Replace pattern in filenames ---
244 # renames = rename_replace_pattern(
245 # files,
246 # find_text="IMG_",
247 # replace_text="vacation_",
248 # case_sensitive=False
249 # )
250
251 # --- MODE 2: Sequential numbering ---
252 renames = rename_sequential(
253 files,
254 pattern="company_retreat_2025_{n}",
255 start_number=1,
256 padding=3
257 )
258
259 # --- MODE 3: Add prefix ---
260 # renames = rename_add_prefix(files, prefix="2025_")
261
262 # --- MODE 4: Add suffix ---
263 # renames = rename_add_suffix(files, suffix="_final")
264
265 # --- MODE 5: Change case ---
266 # renames = rename_change_case(files, case_type='lower')
267
268 # --- MODE 6: Clean filenames ---
269 # renames = rename_clean_filename(files)
270
271 # ========================================
272 # CHECK FOR CONFLICTS
273 # ========================================
274
275 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 return
282
283 # ========================================
284 # EXECUTE (set dry_run=False to apply changes)
285 # ========================================
286
287 execute_renames(renames, dry_run=True)
288
289
290if __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 datetime
2
3def rename_with_date(files, pattern):
4 """Add file modification date to filename."""
5 renames = []
6
7 for file_path in files:
8 mod_time = file_path.stat().st_mtime
9 date_str = datetime.fromtimestamp(mod_time).strftime("%Y%m%d")
10
11 new_name = pattern.replace("{date}", date_str)
12 new_name = f"{new_name}{file_path.suffix}"
13
14 renames.append((file_path, new_name))
15
16 return renames
17
18# Usage: rename_with_date(files, "photo_{date}")
19# Result: photo_20251104.jpgRemove Numbers from Filenames
1def rename_remove_numbers(files):
2 """Remove all numbers from filenames."""
3 renames = []
4
5 for file_path in files:
6 new_name = re.sub(r'\d+', '', file_path.stem)
7 new_name = new_name.strip('_- ') # Clean up leftover separators
8
9 if new_name: # Don't create empty names
10 renames.append((file_path, f"{new_name}{file_path.suffix}"))
11
12 return renamesRegex 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)
5
6 for file_path in files:
7 new_name = pattern.sub(replacement, file_path.stem)
8
9 if new_name != file_path.stem:
10 renames.append((file_path, f"{new_name}{file_path.suffix}"))
11
12 return renames
13
14# 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 json
2
3def 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 renames
8 ]
9
10 with open(undo_path, 'w') as f:
11 json.dump(undo_data, f, indent=2)
12
13 print(f"Undo file saved: {undo_path}")Add Command-Line Arguments
Make the script more flexible:
1import argparse
2
3parser = 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')
9
10args = 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.