Python Script to Rename Multiple Files Automatically
Photoreal minimalist desk with glossy monitor showing glowing abstract code, floating semi-transparent files morphing with golden arrows and a stylized python curve around a folder
Sponsor message — This article is made possible by Dargslan.com, a publisher of practical, no-fluff IT & developer workbooks.
Why Dargslan.com?
If you prefer doing over endless theory, Dargslan’s titles are built for you. Every workbook focuses on skills you can apply the same day—server hardening, Linux one-liners, PowerShell for admins, Python automation, cloud basics, and more.
File management can quickly become overwhelming when you're dealing with dozens, hundreds, or even thousands of files that need consistent naming conventions. Whether you're organizing a photo library, managing project documents, or maintaining a media collection, manually renaming files one by one isn't just tedious—it's a productivity killer that drains your time and energy. The frustration of inconsistent file names, scattered numbering systems, and chaotic folder structures affects professionals across every industry, from photographers and content creators to developers and data analysts.
Automated file renaming through Python scripting represents a powerful solution that transforms this time-consuming task into a streamlined, repeatable process. Python's built-in libraries and straightforward syntax make it accessible even for those with minimal programming experience, while offering the flexibility and power that advanced users demand. This approach allows you to define rules once and apply them consistently across your entire file system, ensuring standardization and saving countless hours of manual work.
Throughout this comprehensive guide, you'll discover multiple approaches to automating file renaming tasks, from basic single-directory operations to advanced batch processing with pattern matching and conditional logic. You'll learn how to safely implement these scripts, handle edge cases and errors, and customize solutions for your specific workflows. Whether you need to add timestamps, remove unwanted characters, standardize extensions, or implement complex naming schemes, you'll find practical, ready-to-use code examples and detailed explanations that empower you to take control of your file organization.
Understanding the Fundamentals of File Renaming in Python
Python provides several built-in modules specifically designed for file system operations, making it an ideal choice for automation tasks. The os module serves as the foundation for most file manipulation operations, offering cross-platform compatibility that works seamlessly across Windows, macOS, and Linux systems. Additionally, the pathlib module introduces an object-oriented approach that many developers find more intuitive and readable, especially when dealing with complex path manipulations.
Before diving into script creation, it's essential to understand the basic operations involved in renaming files programmatically. At its core, file renaming involves three fundamental steps: locating the target files, determining the new names based on your defined rules, and executing the rename operation while handling potential conflicts or errors. Python's exception handling capabilities ensure that your scripts can gracefully manage situations like permission errors, duplicate names, or missing files without crashing or corrupting your file system.
Essential Python Modules for File Operations
The os module provides the most direct interface to operating system functionality, including file and directory manipulation. Its os.rename() function serves as the primary method for changing file names, while os.listdir() helps you retrieve lists of files within directories. The os.path submodule offers utilities for parsing file paths, extracting extensions, and joining path components in a platform-independent manner.
The pathlib module, introduced in Python 3.4, represents a more modern approach to file system operations. It encapsulates paths as objects with intuitive methods and properties, making code more readable and maintainable. The Path class provides methods like .rename(), .glob() for pattern matching, and .iterdir() for directory traversal, all while handling path separators automatically across different operating systems.
For more advanced pattern matching and text manipulation, the re module (regular expressions) becomes invaluable. When you need to identify files based on complex naming patterns or extract specific portions of filenames to transform them, regular expressions provide powerful and flexible solutions. Combined with the glob module for Unix-style pathname pattern expansion, you can create highly specific file selection criteria.
"The ability to automate repetitive file management tasks doesn't just save time—it eliminates human error and ensures consistency across thousands of files that would be impossible to handle manually."
Basic File Renaming Script Structure
Creating your first file renaming script requires understanding the essential components that make up a safe and effective automation tool. A well-structured script begins with proper imports, establishes the target directory, implements file selection logic, defines the renaming pattern, and includes error handling to prevent data loss or system issues.
import os
from pathlib import Path
def rename_files_basic(directory, prefix="file"):
"""
Basic file renaming function that adds a prefix and sequential number
Args:
directory: Path to the directory containing files to rename
prefix: String prefix to add to each filename
"""
try:
# Convert to Path object for easier manipulation
dir_path = Path(directory)
# Check if directory exists
if not dir_path.exists():
raise FileNotFoundError(f"Directory not found: {directory}")
# Get all files in directory (excluding subdirectories)
files = [f for f in dir_path.iterdir() if f.is_file()]
# Sort files to ensure consistent ordering
files.sort()
# Rename each file with prefix and number
for index, file_path in enumerate(files, start=1):
# Preserve the original file extension
extension = file_path.suffix
new_name = f"{prefix}_{index:03d}{extension}"
new_path = dir_path / new_name
# Rename the file
file_path.rename(new_path)
print(f"Renamed: {file_path.name} → {new_name}")
except Exception as e:
print(f"Error occurred: {str(e)}")
# Example usage
if __name__ == "__main__":
rename_files_basic("./my_files", prefix="document")This foundational script demonstrates several important principles. The function accepts parameters that make it reusable for different directories and naming schemes, rather than hardcoding values. The try-except block catches errors gracefully, preventing the script from crashing mid-operation. The use of enumerate() with a start parameter provides clean sequential numbering, while the :03d formatting ensures numbers are zero-padded for proper alphabetical sorting.
Adding Safety Mechanisms
When working with file operations, implementing safety mechanisms is not optional—it's essential. A single error in your renaming logic could potentially overwrite important files or create naming conflicts that corrupt your file system. Professional scripts always include preview modes, backup strategies, and validation checks before executing any destructive operations.
import os
import shutil
from pathlib import Path
from datetime import datetime
def rename_files_safely(directory, prefix="file", dry_run=True, create_backup=True):
"""
Safe file renaming with preview mode and optional backup
Args:
directory: Path to target directory
prefix: Prefix for new filenames
dry_run: If True, only preview changes without executing
create_backup: If True, create backup before renaming
"""
dir_path = Path(directory)
if not dir_path.exists():
raise FileNotFoundError(f"Directory not found: {directory}")
# Create backup if requested
if create_backup and not dry_run:
backup_dir = dir_path.parent / f"{dir_path.name}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
shutil.copytree(dir_path, backup_dir)
print(f"✓ Backup created: {backup_dir}")
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
# Preview or execute renaming
print(f"\n{'PREVIEW MODE' if dry_run else 'EXECUTING RENAME'}")
print("=" * 60)
for index, file_path in enumerate(files, start=1):
extension = file_path.suffix
new_name = f"{prefix}_{index:03d}{extension}"
new_path = dir_path / new_name
# Check for naming conflicts
if new_path.exists() and new_path != file_path:
print(f"⚠ Conflict: {new_name} already exists!")
continue
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
if dry_run:
print("\nThis was a preview. Set dry_run=False to execute.")
# Example usage with safety features
if __name__ == "__main__":
# First, preview the changes
rename_files_safely("./my_files", prefix="document", dry_run=True)
# After reviewing, execute with backup
# rename_files_safely("./my_files", prefix="document", dry_run=False, create_backup=True)The enhanced version introduces a dry_run parameter that allows you to preview all changes before committing them. This preview mode is invaluable for verifying your renaming logic works as expected without risking your actual files. The backup functionality creates a timestamped copy of the entire directory before making any changes, providing a safety net if something goes wrong.
Advanced Pattern-Based Renaming Techniques
Real-world file renaming scenarios often require more sophisticated logic than simple sequential numbering. You might need to extract dates from existing filenames, replace specific text patterns, standardize inconsistent naming conventions, or apply conditional rules based on file properties. Pattern-based renaming leverages regular expressions and conditional logic to handle these complex requirements elegantly.
| Pattern Type | Use Case | Example Transformation |
|---|---|---|
| Date Extraction | Standardizing date formats in filenames | IMG_20231215.jpg → 2023-12-15_IMG.jpg |
| Text Replacement | Removing or replacing specific strings | Document (copy).pdf → Document_backup.pdf |
| Case Normalization | Converting to consistent capitalization | MyFile.TXT → myfile.txt |
| Character Sanitization | Removing illegal or problematic characters | File: Name?.doc → File_Name.doc |
| Extension Correction | Fixing incorrect file extensions | image.jpeg → image.jpg |
Regular Expression-Based Renaming
Regular expressions provide powerful pattern matching capabilities that transform complex renaming tasks into manageable operations. By defining patterns that describe the structure of your current filenames, you can extract specific components and rearrange them according to your desired naming scheme.
import re
from pathlib import Path
from datetime import datetime
def rename_with_patterns(directory, pattern, replacement, dry_run=True):
"""
Rename files using regular expression patterns
Args:
directory: Target directory path
pattern: Regex pattern to match in filenames
replacement: Replacement pattern (can use capture groups)
dry_run: Preview mode flag
"""
dir_path = Path(directory)
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
compiled_pattern = re.compile(pattern)
renamed_count = 0
print(f"\n{'PREVIEW MODE' if dry_run else 'EXECUTING RENAME'}")
print("=" * 60)
for file_path in files:
filename = file_path.stem # Filename without extension
extension = file_path.suffix
# Apply regex pattern
if compiled_pattern.search(filename):
new_filename = compiled_pattern.sub(replacement, filename)
new_name = f"{new_filename}{extension}"
new_path = dir_path / new_name
if new_path.exists() and new_path != file_path:
print(f"⚠ Skipped (conflict): {file_path.name}")
continue
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
renamed_count += 1
print(f"\nTotal files {'would be' if dry_run else ''} renamed: {renamed_count}")
# Example patterns
def standardize_date_format(directory, dry_run=True):
"""Convert dates from YYYYMMDD to YYYY-MM-DD format"""
pattern = r'(\d{4})(\d{2})(\d{2})'
replacement = r'\1-\2-\3'
rename_with_patterns(directory, pattern, replacement, dry_run)
def remove_special_characters(directory, dry_run=True):
"""Remove special characters and replace with underscores"""
pattern = r'[^\w\-.]'
replacement = '_'
rename_with_patterns(directory, pattern, replacement, dry_run)
def extract_and_reorder_dates(directory, dry_run=True):
"""
Extract dates from filenames like 'IMG_20231215_001.jpg'
and reformat to '2023-12-15_IMG_001.jpg'
"""
dir_path = Path(directory)
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
date_pattern = re.compile(r'(\w+)_(\d{4})(\d{2})(\d{2})_(\d+)')
for file_path in files:
match = date_pattern.match(file_path.stem)
if match:
prefix, year, month, day, number = match.groups()
new_name = f"{year}-{month}-{day}_{prefix}_{number}{file_path.suffix}"
new_path = dir_path / new_name
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
# Example usage
if __name__ == "__main__":
standardize_date_format("./photos", dry_run=True)
remove_special_characters("./documents", dry_run=True)
extract_and_reorder_dates("./camera_images", dry_run=True)"Pattern matching transforms file renaming from a tedious manual task into an intelligent automation that understands the structure and meaning embedded in your existing filenames."
Conditional Renaming Based on File Properties
Sometimes the appropriate naming scheme depends not just on the current filename, but on the file's properties such as size, creation date, modification date, or even content type. Python's os.stat() function and the pathlib module provide access to file metadata that enables sophisticated conditional renaming logic.
import os
from pathlib import Path
from datetime import datetime
def rename_by_date_modified(directory, date_format="%Y-%m-%d", dry_run=True):
"""
Rename files based on their modification date
Args:
directory: Target directory
date_format: Format string for date portion of filename
dry_run: Preview mode flag
"""
dir_path = Path(directory)
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
# Track counters for files with same date
date_counters = {}
print(f"\n{'PREVIEW MODE' if dry_run else 'EXECUTING RENAME'}")
print("=" * 60)
for file_path in files:
# Get modification time
mod_time = datetime.fromtimestamp(file_path.stat().st_mtime)
date_str = mod_time.strftime(date_format)
# Increment counter for this date
date_counters[date_str] = date_counters.get(date_str, 0) + 1
counter = date_counters[date_str]
# Create new filename with date and counter
extension = file_path.suffix
new_name = f"{date_str}_{counter:03d}{extension}"
new_path = dir_path / new_name
if new_path.exists() and new_path != file_path:
print(f"⚠ Skipped (conflict): {file_path.name}")
continue
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
def rename_by_file_size(directory, dry_run=True):
"""Categorize and rename files based on size"""
dir_path = Path(directory)
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
def get_size_category(size_bytes):
"""Determine size category for file"""
if size_bytes < 1024 * 1024: # < 1MB
return "small"
elif size_bytes < 10 * 1024 * 1024: # < 10MB
return "medium"
else:
return "large"
# Track counters for each category
category_counters = {}
for file_path in files:
size_bytes = file_path.stat().st_size
category = get_size_category(size_bytes)
category_counters[category] = category_counters.get(category, 0) + 1
counter = category_counters[category]
extension = file_path.suffix
new_name = f"{category}_{counter:03d}{extension}"
new_path = dir_path / new_name
if dry_run:
size_mb = size_bytes / (1024 * 1024)
print(f"Would rename: {file_path.name} ({size_mb:.2f}MB) → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
def rename_by_file_type(directory, dry_run=True):
"""Organize files by extension with categorized naming"""
dir_path = Path(directory)
files = sorted([f for f in dir_path.iterdir() if f.is_file()])
# Define file type categories
file_categories = {
'images': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg'],
'documents': ['.pdf', '.doc', '.docx', '.txt', '.rtf'],
'spreadsheets': ['.xls', '.xlsx', '.csv'],
'videos': ['.mp4', '.avi', '.mov', '.mkv'],
'audio': ['.mp3', '.wav', '.flac', '.aac'],
}
# Track counters for each category
category_counters = {}
for file_path in files:
extension = file_path.suffix.lower()
# Determine category
category = 'other'
for cat_name, extensions in file_categories.items():
if extension in extensions:
category = cat_name
break
category_counters[category] = category_counters.get(category, 0) + 1
counter = category_counters[category]
new_name = f"{category}_{counter:03d}{extension}"
new_path = dir_path / new_name
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
# Example usage
if __name__ == "__main__":
rename_by_date_modified("./mixed_files", dry_run=True)
rename_by_file_size("./downloads", dry_run=True)
rename_by_file_type("./unsorted", dry_run=True)These conditional renaming functions demonstrate how file metadata can inform intelligent naming decisions. The date-based approach is particularly useful for organizing photos or documents chronologically, while size-based categorization helps identify and manage large files. Type-based renaming automatically organizes mixed directories into logical categories without requiring manual sorting.
Batch Processing Multiple Directories
When working with complex project structures or large file collections, you often need to apply renaming operations across multiple directories simultaneously. Recursive directory traversal combined with filtering logic allows you to process entire folder hierarchies while maintaining control over which files get renamed and which directories get processed.
import os
from pathlib import Path
from typing import List, Callable
def rename_recursive(root_directory,
rename_function: Callable,
file_pattern="*",
exclude_dirs: List[str] = None,
max_depth: int = None,
dry_run=True):
"""
Recursively rename files across directory structure
Args:
root_directory: Starting directory for recursive search
rename_function: Function that takes a file path and returns new name
file_pattern: Glob pattern for file selection (e.g., "*.txt")
exclude_dirs: List of directory names to skip
max_depth: Maximum recursion depth (None for unlimited)
dry_run: Preview mode flag
"""
root_path = Path(root_directory)
exclude_dirs = exclude_dirs or []
def process_directory(dir_path: Path, current_depth: int = 0):
"""Recursively process directory and subdirectories"""
# Check depth limit
if max_depth is not None and current_depth > max_depth:
return
# Skip excluded directories
if dir_path.name in exclude_dirs:
print(f"⊗ Skipping excluded directory: {dir_path}")
return
print(f"\n📁 Processing: {dir_path}")
# Process files in current directory
files = sorted(dir_path.glob(file_pattern))
files = [f for f in files if f.is_file()]
for file_path in files:
try:
new_name = rename_function(file_path)
if new_name and new_name != file_path.name:
new_path = file_path.parent / new_name
if new_path.exists() and new_path != file_path:
print(f" ⚠ Conflict: {new_name} already exists")
continue
if dry_run:
print(f" Would rename: {file_path.name} → {new_name}")
else:
file_path.rename(new_path)
print(f" ✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
print(f" ✗ Error processing {file_path.name}: {str(e)}")
# Recursively process subdirectories
subdirs = [d for d in dir_path.iterdir() if d.is_dir()]
for subdir in subdirs:
process_directory(subdir, current_depth + 1)
print(f"\n{'='*60}")
print(f"{'PREVIEW MODE' if dry_run else 'EXECUTING RENAME'}")
print(f"Root: {root_path}")
print(f"Pattern: {file_pattern}")
print(f"{'='*60}")
process_directory(root_path)
# Example rename functions for use with recursive processing
def lowercase_rename(file_path: Path) -> str:
"""Convert filename to lowercase"""
return file_path.name.lower()
def add_prefix_rename(prefix: str):
"""Factory function to create prefix-adding rename function"""
def rename_func(file_path: Path) -> str:
return f"{prefix}_{file_path.name}"
return rename_func
def remove_spaces_rename(file_path: Path) -> str:
"""Replace spaces with underscores"""
return file_path.name.replace(' ', '_')
def standardize_extensions_rename(file_path: Path) -> str:
"""Standardize file extensions to lowercase"""
stem = file_path.stem
extension = file_path.suffix.lower()
return f"{stem}{extension}"
# Advanced example: Combine multiple transformations
def comprehensive_rename(file_path: Path) -> str:
"""Apply multiple transformations in sequence"""
name = file_path.name
# Remove special characters
name = re.sub(r'[^\w\s\-.]', '', name)
# Replace spaces with underscores
name = name.replace(' ', '_')
# Convert to lowercase
name = name.lower()
# Standardize common extension variations
extension_map = {
'.jpeg': '.jpg',
'.htm': '.html',
'.mpeg': '.mpg',
}
stem = Path(name).stem
ext = Path(name).suffix
ext = extension_map.get(ext, ext)
return f"{stem}{ext}"
# Example usage
if __name__ == "__main__":
# Process all text files recursively, converting to lowercase
rename_recursive(
"./project_files",
rename_function=lowercase_rename,
file_pattern="*.txt",
exclude_dirs=['backup', 'archive'],
max_depth=3,
dry_run=True
)
# Add project prefix to all Python files
rename_recursive(
"./code",
rename_function=add_prefix_rename("proj"),
file_pattern="*.py",
dry_run=True
)
# Comprehensive cleanup of all files
rename_recursive(
"./documents",
rename_function=comprehensive_rename,
file_pattern="*",
exclude_dirs=['templates', 'node_modules'],
dry_run=True
)"Recursive batch processing transforms hours of manual file organization across complex directory structures into a single script execution that consistently applies your naming standards everywhere."
Handling Large-Scale Operations
When processing thousands of files, performance and progress tracking become important considerations. Adding progress indicators, logging capabilities, and performance optimizations ensures your scripts remain responsive and provide visibility into long-running operations.
import os
import logging
from pathlib import Path
from datetime import datetime
from typing import Callable, List
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'rename_log_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'),
logging.StreamHandler()
]
)
def rename_with_progress(root_directory,
rename_function: Callable,
file_pattern="*",
dry_run=True):
"""
Rename files with progress tracking and detailed logging
"""
root_path = Path(root_directory)
# First pass: count total files
logging.info(f"Scanning directory structure: {root_path}")
all_files = list(root_path.rglob(file_pattern))
all_files = [f for f in all_files if f.is_file()]
total_files = len(all_files)
logging.info(f"Found {total_files} files matching pattern '{file_pattern}'")
if total_files == 0:
logging.warning("No files found to process")
return
# Process files with progress tracking
successful = 0
skipped = 0
errors = 0
for index, file_path in enumerate(all_files, 1):
# Progress indicator
progress = (index / total_files) * 100
print(f"\rProgress: {progress:.1f}% ({index}/{total_files})", end='', flush=True)
try:
new_name = rename_function(file_path)
if not new_name or new_name == file_path.name:
skipped += 1
continue
new_path = file_path.parent / new_name
if new_path.exists() and new_path != file_path:
logging.warning(f"Name conflict: {new_name} already exists for {file_path}")
skipped += 1
continue
if dry_run:
logging.info(f"Would rename: {file_path} → {new_name}")
else:
file_path.rename(new_path)
logging.info(f"Renamed: {file_path} → {new_name}")
successful += 1
except PermissionError:
logging.error(f"Permission denied: {file_path}")
errors += 1
except Exception as e:
logging.error(f"Error processing {file_path}: {str(e)}")
errors += 1
# Summary
print("\n") # New line after progress indicator
logging.info("="*60)
logging.info("OPERATION SUMMARY")
logging.info("="*60)
logging.info(f"Total files processed: {total_files}")
logging.info(f"Successfully renamed: {successful}")
logging.info(f"Skipped: {skipped}")
logging.info(f"Errors: {errors}")
if dry_run:
logging.info("This was a DRY RUN - no files were actually renamed")
# Example usage
if __name__ == "__main__":
rename_with_progress(
"./large_project",
rename_function=comprehensive_rename,
file_pattern="*",
dry_run=True
)Specialized Renaming Scenarios
Different industries and use cases require specialized renaming approaches. Photographers need to organize images by EXIF data, developers need to maintain consistent code file naming, and media professionals need to standardize video and audio file names according to production workflows. These specialized scenarios benefit from custom functions tailored to their specific requirements.
🖼️ Photo Organization by EXIF Data
Digital photos contain rich metadata embedded in EXIF tags, including capture date, camera model, exposure settings, and GPS coordinates. Leveraging this metadata for file naming creates organized photo libraries that preserve important information about when and how each image was captured.
from pathlib import Path
from datetime import datetime
from PIL import Image
from PIL.ExifTags import TAGS
import logging
def get_exif_data(image_path: Path) -> dict:
"""Extract EXIF data from image file"""
try:
image = Image.open(image_path)
exif_data = {}
if hasattr(image, '_getexif') and image._getexif() is not None:
exif = image._getexif()
for tag_id, value in exif.items():
tag = TAGS.get(tag_id, tag_id)
exif_data[tag] = value
return exif_data
except Exception as e:
logging.error(f"Error reading EXIF from {image_path}: {str(e)}")
return {}
def rename_photos_by_exif(directory, dry_run=True):
"""
Rename photos using EXIF date and camera information
Format: YYYY-MM-DD_HHMMSS_CameraModel_###.ext
"""
dir_path = Path(directory)
image_extensions = ['.jpg', '.jpeg', '.png', '.tiff', '.raw', '.cr2', '.nef']
files = sorted([f for f in dir_path.iterdir()
if f.is_file() and f.suffix.lower() in image_extensions])
# Track counter for images with same timestamp
timestamp_counters = {}
for file_path in files:
exif_data = get_exif_data(file_path)
# Extract date/time
date_str = exif_data.get('DateTimeOriginal') or exif_data.get('DateTime')
if date_str:
try:
# Parse EXIF date format: "YYYY:MM:DD HH:MM:SS"
dt = datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
date_part = dt.strftime("%Y-%m-%d_%H%M%S")
except ValueError:
logging.warning(f"Could not parse date for {file_path.name}")
date_part = "unknown_date"
else:
# Fallback to file modification time
mod_time = datetime.fromtimestamp(file_path.stat().st_mtime)
date_part = mod_time.strftime("%Y-%m-%d_%H%M%S")
# Extract camera model
camera_model = exif_data.get('Model', 'unknown_camera')
camera_model = camera_model.replace(' ', '_').lower()
# Create unique counter for this timestamp
timestamp_counters[date_part] = timestamp_counters.get(date_part, 0) + 1
counter = timestamp_counters[date_part]
# Build new filename
extension = file_path.suffix.lower()
new_name = f"{date_part}_{camera_model}_{counter:03d}{extension}"
new_path = dir_path / new_name
if dry_run:
print(f"Would rename: {file_path.name} → {new_name}")
else:
if new_path.exists():
logging.warning(f"Conflict: {new_name} already exists")
continue
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
# Example usage
if __name__ == "__main__":
rename_photos_by_exif("./photo_library", dry_run=True)💻 Code File Organization
Development projects benefit from consistent naming conventions that reflect module hierarchy, component types, and functionality. Automated renaming helps maintain these standards across large codebases.
from pathlib import Path
import re
def rename_code_files(directory, naming_convention='snake_case', dry_run=True):
"""
Standardize code file naming conventions
Args:
directory: Target directory
naming_convention: 'snake_case', 'camelCase', or 'kebab-case'
dry_run: Preview mode
"""
dir_path = Path(directory)
code_extensions = ['.py', '.js', '.ts', '.java', '.cpp', '.c', '.h', '.rb', '.php']
files = sorted([f for f in dir_path.rglob('*')
if f.is_file() and f.suffix.lower() in code_extensions])
def to_snake_case(name: str) -> str:
"""Convert to snake_case"""
# Insert underscore before uppercase letters
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name)
# Replace hyphens and spaces with underscores
name = name.replace('-', '_').replace(' ', '_')
return name.lower()
def to_camel_case(name: str) -> str:
"""Convert to camelCase"""
parts = re.split('[_\\-\\s]+', name.lower())
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
def to_kebab_case(name: str) -> str:
"""Convert to kebab-case"""
name = to_snake_case(name)
return name.replace('_', '-')
converters = {
'snake_case': to_snake_case,
'camelCase': to_camel_case,
'kebab-case': to_kebab_case
}
converter = converters.get(naming_convention, to_snake_case)
for file_path in files:
stem = file_path.stem
extension = file_path.suffix
# Apply naming convention
new_stem = converter(stem)
new_name = f"{new_stem}{extension}"
new_path = file_path.parent / new_name
if new_name != file_path.name:
if dry_run:
print(f"Would rename: {file_path.relative_to(dir_path)} → {new_name}")
else:
if new_path.exists():
print(f"⚠ Conflict: {new_name} already exists")
continue
file_path.rename(new_path)
print(f"✓ Renamed: {file_path.name} → {new_name}")
# Example usage
if __name__ == "__main__":
rename_code_files("./src", naming_convention='snake_case', dry_run=True)🎬 Media File Standardization
Video and audio files often come with inconsistent naming from various sources. Standardizing these files with meaningful metadata improves media library organization and searchability.
from pathlib import Path
import re
from datetime import datetime
def rename_media_files(directory, media_type='video', dry_run=True):
"""
Standardize media file naming with metadata
Args:
directory: Target directory
media_type: 'video' or 'audio'
dry_run: Preview mode
"""
dir_path = Path(directory)
extensions = {
'video': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm'],
'audio': ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma']
}
valid_extensions = extensions.get(media_type, extensions['video'])
files = sorted([f for f in dir_path.iterdir()
if f.is_file() and f.suffix.lower() in valid_extensions])
def extract_resolution(filename: str) -> str:
"""Extract video resolution if present"""
patterns = [
(r'4k|2160p', '4K'),
(r'1440p|2k', '2K'),
(r'1080p|fullhd|fhd', '1080p'),
(r'720p|hd', '720p'),
(r'480p|sd', '480p')
]
filename_lower = filename.lower()
for pattern, resolution in patterns:
if re.search(pattern, filename_lower):
return resolution
return 'unknown'
def extract_year(filename: str) -> str:
"""Extract year if present (1900-2099)"""
match = re.search(r'\b(19|20)\d{2}\b', filename)
return match.group(0) if match else ''
def clean_title(filename: str) -> str:
"""Clean and format title"""
# Remove extension
title = Path(filename).stem
# Remove common patterns
title = re.sub(r'\[.*?\]|\(.*?\)', '', title) # Remove brackets
title = re.sub(r'\d{3,4}p|4k|hd|fhd', '', title, flags=re.IGNORECASE) # Remove quality indicators
title = re.sub(r'\b(19|20)\d{2}\b', '', title) # Remove years
# Clean up spacing and special characters
title = re.sub(r'[._\-]+', ' ', title)
title = re.sub(r'\s+', ' ', title).strip()
# Convert to title case
title = title.title()
# Replace spaces with underscores
title = title.replace(' ', '_')
return title
for file_path in files:
filename = file_path.name
extension = file_path.suffix.lower()
# Extract components
title = clean_title(filename)
year = extract_year(filename)
if media_type == 'video':
resolution = extract_resolution(filename)
components = [title]
if year:
components.append(year)
if resolution != 'unknown':
components.append(resolution)
new_name = '_'.join(components) + extension
else: # audio
components = [title]
if year:
components.append(year)
new_name = '_'.join(components) + extension
new_path = dir_path / new_name
if new_name != filename:
if dry_run:
print(f"Would rename: {filename}")
print(f" to: {new_name}\n")
else:
if new_path.exists():
print(f"⚠ Conflict: {new_name} already exists")
continue
file_path.rename(new_path)
print(f"✓ Renamed: {filename} → {new_name}")
# Example usage
if __name__ == "__main__":
rename_media_files("./videos", media_type='video', dry_run=True)
rename_media_files("./music", media_type='audio', dry_run=True)
| Scenario | Key Considerations | Recommended Approach |
|---|---|---|
| Photo Libraries | Preserve date/time, camera info, avoid duplicates | EXIF-based naming with timestamp and camera model |
| Code Projects | Consistent conventions, module hierarchy, readability | Case conversion with namespace prefixes |
| Media Collections | Metadata extraction, quality indicators, searchability | Structured naming with title, year, and quality |
| Document Archives | Date organization, version control, categorization | Date-prefixed with category and version numbers |
| Backup Files | Timestamp accuracy, source identification, retention | ISO date format with source identifier and hash |
"Specialized renaming functions transform generic file collections into professionally organized libraries that reflect the specific requirements and workflows of your industry or use case."
Error Handling and Recovery Strategies
Robust file renaming scripts must anticipate and gracefully handle various error conditions that can occur during execution. Permission errors, file locks, naming conflicts, and unexpected exceptions can all interrupt operations if not properly managed. Implementing comprehensive error handling and recovery mechanisms ensures your scripts remain reliable even when encountering problematic files or system conditions.
import os
import shutil
from pathlib import Path
from datetime import datetime
import json
import logging
class FileRenameManager:
"""
Advanced file rename manager with transaction-like behavior
and rollback capabilities
"""
def __init__(self, directory, log_file=None):
self.directory = Path(directory)
self.log_file = log_file or f"rename_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
self.operations = []
self.successful = []
self.failed = []
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
def add_rename_operation(self, old_path: Path, new_name: str):
"""Queue a rename operation"""
self.operations.append({
'old_path': old_path,
'new_name': new_name,
'new_path': old_path.parent / new_name,
'timestamp': datetime.now().isoformat()
})
def validate_operations(self):
"""Validate all queued operations before execution"""
self.logger.info("Validating rename operations...")
conflicts = []
invalid = []
for op in self.operations:
old_path = op['old_path']
new_path = op['new_path']
# Check if source file exists
if not old_path.exists():
invalid.append({
'operation': op,
'reason': 'Source file does not exist'
})
continue
# Check for naming conflicts
if new_path.exists() and new_path != old_path:
conflicts.append({
'operation': op,
'reason': f'Target name already exists: {new_path.name}'
})
# Check write permissions
try:
# Test write access to parent directory
test_file = old_path.parent / f'.test_write_{os.getpid()}'
test_file.touch()
test_file.unlink()
except PermissionError:
invalid.append({
'operation': op,
'reason': 'Insufficient permissions'
})
if conflicts:
self.logger.warning(f"Found {len(conflicts)} naming conflicts")
for conflict in conflicts:
self.logger.warning(f" {conflict['operation']['old_path'].name} → {conflict['reason']}")
if invalid:
self.logger.error(f"Found {len(invalid)} invalid operations")
for item in invalid:
self.logger.error(f" {item['operation']['old_path'].name}: {item['reason']}")
return len(conflicts) == 0 and len(invalid) == 0
def execute_operations(self, create_backup=True):
"""Execute all queued rename operations with error handling"""
if not self.validate_operations():
self.logger.error("Validation failed. Aborting operations.")
return False
# Create backup if requested
if create_backup:
backup_dir = self.directory.parent / f"{self.directory.name}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
try:
shutil.copytree(self.directory, backup_dir)
self.logger.info(f"Backup created: {backup_dir}")
except Exception as e:
self.logger.error(f"Backup creation failed: {str(e)}")
return False
self.logger.info(f"Executing {len(self.operations)} rename operations...")
for op in self.operations:
try:
old_path = op['old_path']
new_path = op['new_path']
# Perform rename
old_path.rename(new_path)
# Record success
op['status'] = 'success'
op['completed_at'] = datetime.now().isoformat()
self.successful.append(op)
self.logger.info(f"✓ Renamed: {old_path.name} → {new_path.name}")
except PermissionError as e:
op['status'] = 'failed'
op['error'] = f"Permission denied: {str(e)}"
self.failed.append(op)
self.logger.error(f"✗ Permission denied: {old_path.name}")
except FileExistsError as e:
op['status'] = 'failed'
op['error'] = f"File already exists: {str(e)}"
self.failed.append(op)
self.logger.error(f"✗ File exists: {new_path.name}")
except Exception as e:
op['status'] = 'failed'
op['error'] = str(e)
self.failed.append(op)
self.logger.error(f"✗ Unexpected error for {old_path.name}: {str(e)}")
# Save operation log
self.save_log()
# Print summary
self.print_summary()
return len(self.failed) == 0
def rollback(self):
"""Rollback successful operations"""
self.logger.info("Initiating rollback...")
rollback_count = 0
rollback_failed = 0
for op in reversed(self.successful):
try:
new_path = op['new_path']
old_path = op['old_path']
if new_path.exists():
new_path.rename(old_path)
rollback_count += 1
self.logger.info(f"↶ Rolled back: {new_path.name} → {old_path.name}")
except Exception as e:
rollback_failed += 1
self.logger.error(f"✗ Rollback failed for {new_path.name}: {str(e)}")
self.logger.info(f"Rollback complete: {rollback_count} operations reversed, {rollback_failed} failed")
def save_log(self):
"""Save operation log to JSON file"""
log_data = {
'timestamp': datetime.now().isoformat(),
'directory': str(self.directory),
'total_operations': len(self.operations),
'successful': len(self.successful),
'failed': len(self.failed),
'operations': []
}
for op in self.operations:
log_entry = {
'old_name': op['old_path'].name,
'new_name': op['new_name'],
'status': op.get('status', 'pending'),
'timestamp': op['timestamp']
}
if 'error' in op:
log_entry['error'] = op['error']
log_data['operations'].append(log_entry)
with open(self.log_file, 'w') as f:
json.dump(log_data, f, indent=2)
self.logger.info(f"Operation log saved: {self.log_file}")
def print_summary(self):
"""Print operation summary"""
self.logger.info("="*60)
self.logger.info("OPERATION SUMMARY")
self.logger.info("="*60)
self.logger.info(f"Total operations: {len(self.operations)}")
self.logger.info(f"Successful: {len(self.successful)}")
self.logger.info(f"Failed: {len(self.failed)}")
if self.failed:
self.logger.info("\nFailed operations:")
for op in self.failed:
self.logger.info(f" {op['old_path'].name}: {op.get('error', 'Unknown error')}")
# Example usage with error handling
def safe_rename_example():
"""Example demonstrating safe renaming with error handling"""
manager = FileRenameManager("./test_files")
# Queue rename operations
files = sorted(Path("./test_files").iterdir())
for index, file_path in enumerate(files, 1):
if file_path.is_file():
new_name = f"document_{index:03d}{file_path.suffix}"
manager.add_rename_operation(file_path, new_name)
# Execute with backup
success = manager.execute_operations(create_backup=True)
if not success:
user_input = input("Some operations failed. Rollback? (y/n): ")
if user_input.lower() == 'y':
manager.rollback()
if __name__ == "__main__":
safe_rename_example()"Professional file renaming scripts don't just execute operations—they anticipate failures, provide detailed logging, and offer rollback capabilities that protect your data even when things go wrong."
Creating a Reusable Command-Line Tool
Transforming your file renaming scripts into a polished command-line tool makes them more accessible and practical for daily use. By implementing proper argument parsing, configuration options, and user-friendly output, you create a utility that can be easily shared and integrated into various workflows without requiring code modifications for each use case.
#!/usr/bin/env python3
"""
File Renaming Utility - Command-line tool for batch file renaming
"""
import argparse
import sys
from pathlib import Path
from typing import List
import re
import logging
class FileRenamer:
"""Main file renaming utility class"""
def __init__(self, verbose=False):
self.verbose = verbose
self.setup_logging()
def setup_logging(self):
"""Configure logging based on verbosity"""
level = logging.DEBUG if self.verbose else logging.INFO
logging.basicConfig(
level=level,
format='%(message)s'
)
self.logger = logging.getLogger(__name__)
def rename_sequential(self, directory: str, prefix: str, start: int = 1,
pattern: str = "*", dry_run: bool = True):
"""Rename files with sequential numbering"""
dir_path = Path(directory)
files = sorted(dir_path.glob(pattern))
files = [f for f in files if f.is_file()]
self.logger.info(f"\n{'DRY RUN - ' if dry_run else ''}Renaming {len(files)} files")
self.logger.info("="*60)
for index, file_path in enumerate(files, start=start):
new_name = f"{prefix}_{index:03d}{file_path.suffix}"
new_path = dir_path / new_name
if dry_run:
self.logger.info(f"Would rename: {file_path.name} → {new_name}")
else:
try:
file_path.rename(new_path)
self.logger.info(f"✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
self.logger.error(f"✗ Error: {file_path.name}: {str(e)}")
def rename_replace(self, directory: str, find: str, replace: str,
pattern: str = "*", dry_run: bool = True):
"""Replace text in filenames"""
dir_path = Path(directory)
files = sorted(dir_path.glob(pattern))
files = [f for f in files if f.is_file()]
self.logger.info(f"\n{'DRY RUN - ' if dry_run else ''}Replacing '{find}' with '{replace}'")
self.logger.info("="*60)
for file_path in files:
if find in file_path.name:
new_name = file_path.name.replace(find, replace)
new_path = dir_path / new_name
if dry_run:
self.logger.info(f"Would rename: {file_path.name} → {new_name}")
else:
try:
file_path.rename(new_path)
self.logger.info(f"✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
self.logger.error(f"✗ Error: {file_path.name}: {str(e)}")
def rename_lowercase(self, directory: str, pattern: str = "*",
dry_run: bool = True):
"""Convert filenames to lowercase"""
dir_path = Path(directory)
files = sorted(dir_path.glob(pattern))
files = [f for f in files if f.is_file()]
self.logger.info(f"\n{'DRY RUN - ' if dry_run else ''}Converting to lowercase")
self.logger.info("="*60)
for file_path in files:
new_name = file_path.name.lower()
if new_name != file_path.name:
new_path = dir_path / new_name
if dry_run:
self.logger.info(f"Would rename: {file_path.name} → {new_name}")
else:
try:
file_path.rename(new_path)
self.logger.info(f"✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
self.logger.error(f"✗ Error: {file_path.name}: {str(e)}")
def rename_regex(self, directory: str, regex_pattern: str,
replacement: str, pattern: str = "*", dry_run: bool = True):
"""Rename using regular expressions"""
dir_path = Path(directory)
files = sorted(dir_path.glob(pattern))
files = [f for f in files if f.is_file()]
try:
compiled_pattern = re.compile(regex_pattern)
except re.error as e:
self.logger.error(f"Invalid regex pattern: {str(e)}")
return
self.logger.info(f"\n{'DRY RUN - ' if dry_run else ''}Applying regex pattern")
self.logger.info("="*60)
for file_path in files:
stem = file_path.stem
if compiled_pattern.search(stem):
new_stem = compiled_pattern.sub(replacement, stem)
new_name = f"{new_stem}{file_path.suffix}"
new_path = dir_path / new_name
if dry_run:
self.logger.info(f"Would rename: {file_path.name} → {new_name}")
else:
try:
file_path.rename(new_path)
self.logger.info(f"✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
self.logger.error(f"✗ Error: {file_path.name}: {str(e)}")
def main():
"""Main entry point for command-line tool"""
parser = argparse.ArgumentParser(
description='Batch file renaming utility',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Preview sequential renaming
%(prog)s sequential ./photos --prefix "photo" --dry-run
# Replace text in filenames
%(prog)s replace ./docs --find " " --replace "_" --execute
# Convert to lowercase
%(prog)s lowercase ./files --pattern "*.txt" --execute
# Regex-based renaming
%(prog)s regex ./files --pattern "IMG_(\\d+)" --replace "image_\\1" --execute
"""
)
parser.add_argument('--version', action='version', version='%(prog)s 1.0')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
subparsers = parser.add_subparsers(dest='command', help='Renaming mode')
# Sequential renaming
sequential = subparsers.add_parser('sequential',
help='Rename with sequential numbers')
sequential.add_argument('directory', help='Target directory')
sequential.add_argument('--prefix', default='file',
help='Prefix for new filenames (default: file)')
sequential.add_argument('--start', type=int, default=1,
help='Starting number (default: 1)')
sequential.add_argument('--pattern', default='*',
help='File pattern to match (default: *)')
sequential.add_argument('--dry-run', action='store_true',
help='Preview changes without executing')
sequential.add_argument('--execute', action='store_true',
help='Execute the rename operation')
# Replace text
replace = subparsers.add_parser('replace', help='Replace text in filenames')
replace.add_argument('directory', help='Target directory')
replace.add_argument('--find', required=True, help='Text to find')
replace.add_argument('--replace', required=True, help='Replacement text')
replace.add_argument('--pattern', default='*',
help='File pattern to match (default: *)')
replace.add_argument('--dry-run', action='store_true',
help='Preview changes without executing')
replace.add_argument('--execute', action='store_true',
help='Execute the rename operation')
# Lowercase conversion
lowercase = subparsers.add_parser('lowercase',
help='Convert filenames to lowercase')
lowercase.add_argument('directory', help='Target directory')
lowercase.add_argument('--pattern', default='*',
help='File pattern to match (default: *)')
lowercase.add_argument('--dry-run', action='store_true',
help='Preview changes without executing')
lowercase.add_argument('--execute', action='store_true',
help='Execute the rename operation')
# Regex renaming
regex = subparsers.add_parser('regex', help='Rename using regular expressions')
regex.add_argument('directory', help='Target directory')
regex.add_argument('--pattern', required=True, help='Regex pattern to match')
regex.add_argument('--replace', required=True, help='Replacement pattern')
regex.add_argument('--file-pattern', default='*',
help='File pattern to match (default: *)')
regex.add_argument('--dry-run', action='store_true',
help='Preview changes without executing')
regex.add_argument('--execute', action='store_true',
help='Execute the rename operation')
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# Create renamer instance
renamer = FileRenamer(verbose=args.verbose)
# Determine dry_run status (default to True if neither flag specified)
dry_run = not args.execute if hasattr(args, 'execute') else True
# Execute appropriate command
if args.command == 'sequential':
renamer.rename_sequential(
args.directory,
args.prefix,
args.start,
args.pattern,
dry_run
)
elif args.command == 'replace':
renamer.rename_replace(
args.directory,
args.find,
args.replace,
args.pattern,
dry_run
)
elif args.command == 'lowercase':
renamer.rename_lowercase(
args.directory,
args.pattern,
dry_run
)
elif args.command == 'regex':
renamer.rename_regex(
args.directory,
args.pattern,
args.replace,
args.file_pattern,
dry_run
)
if __name__ == '__main__':
main()This command-line tool provides a professional interface that can be used directly from the terminal without modifying code. The argparse module creates a clean argument structure with built-in help documentation, making the tool self-documenting and user-friendly. The subcommand architecture allows different renaming modes while maintaining a consistent interface across all operations.
Performance Optimization for Large-Scale Operations
When dealing with tens of thousands of files or more, performance considerations become critical. Inefficient code that works fine for hundreds of files can become prohibitively slow at scale. Optimizing file operations, implementing parallel processing, and using efficient data structures can dramatically improve execution time for large-scale renaming tasks.
⚡ Parallel Processing Implementation
import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Callable, List
import logging
from datetime import datetime
class ParallelFileRenamer:
"""
High-performance file renamer using parallel processing
"""
def __init__(self, max_workers=None):
"""
Initialize parallel renamer
Args:
max_workers: Maximum number of parallel workers (default: CPU count)
"""
self.max_workers = max_workers or os.cpu_count()
self.logger = logging.getLogger(__name__)
def rename_batch(self, files: List[Path],
rename_function: Callable[[Path], str],
dry_run: bool = True) -> dict:
"""
Rename files in parallel
Args:
files: List of file paths to rename
rename_function: Function that takes Path and returns new filename
dry_run: Preview mode flag
Returns:
Dictionary with operation statistics
"""
start_time = datetime.now()
stats = {
'total': len(files),
'successful': 0,
'failed': 0,
'skipped': 0,
'errors': []
}
def process_file(file_path: Path) -> tuple:
"""Process single file rename"""
try:
new_name = rename_function(file_path)
if not new_name or new_name == file_path.name:
return ('skipped', file_path, None)
new_path = file_path.parent / new_name
if new_path.exists() and new_path != file_path:
return ('failed', file_path, 'Name conflict')
if not dry_run:
file_path.rename(new_path)
return ('success', file_path, new_name)
except Exception as e:
return ('failed', file_path, str(e))
# Process files in parallel
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# Submit all tasks
future_to_file = {executor.submit(process_file, f): f for f in files}
# Process completed tasks
for future in as_completed(future_to_file):
status, file_path, result = future.result()
if status == 'success':
stats['successful'] += 1
if not dry_run:
self.logger.info(f"✓ Renamed: {file_path.name} → {result}")
else:
self.logger.info(f"Would rename: {file_path.name} → {result}")
elif status == 'failed':
stats['failed'] += 1
stats['errors'].append({
'file': str(file_path),
'error': result
})
self.logger.error(f"✗ Failed: {file_path.name} - {result}")
else: # skipped
stats['skipped'] += 1
# Calculate execution time
duration = (datetime.now() - start_time).total_seconds()
stats['duration'] = duration
stats['files_per_second'] = len(files) / duration if duration > 0 else 0
return stats
def rename_directory_parallel(self, directory: str,
rename_function: Callable,
pattern: str = "*",
recursive: bool = False,
dry_run: bool = True):
"""
Rename all files in directory using parallel processing
"""
dir_path = Path(directory)
# Collect files
if recursive:
files = [f for f in dir_path.rglob(pattern) if f.is_file()]
else:
files = [f for f in dir_path.glob(pattern) if f.is_file()]
files.sort()
self.logger.info(f"\n{'DRY RUN - ' if dry_run else ''}Processing {len(files)} files")
self.logger.info(f"Using {self.max_workers} parallel workers")
self.logger.info("="*60)
# Execute parallel rename
stats = self.rename_batch(files, rename_function, dry_run)
# Print summary
self.logger.info("\n" + "="*60)
self.logger.info("PERFORMANCE SUMMARY")
self.logger.info("="*60)
self.logger.info(f"Total files: {stats['total']}")
self.logger.info(f"Successful: {stats['successful']}")
self.logger.info(f"Failed: {stats['failed']}")
self.logger.info(f"Skipped: {stats['skipped']}")
self.logger.info(f"Duration: {stats['duration']:.2f} seconds")
self.logger.info(f"Speed: {stats['files_per_second']:.2f} files/second")
if stats['errors']:
self.logger.info(f"\nErrors encountered: {len(stats['errors'])}")
for error in stats['errors'][:10]: # Show first 10 errors
self.logger.info(f" {error['file']}: {error['error']}")
# Example usage
def performance_example():
"""Example demonstrating parallel processing"""
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Create parallel renamer
renamer = ParallelFileRenamer(max_workers=8)
# Define rename function
def add_timestamp(file_path: Path) -> str:
timestamp = datetime.now().strftime("%Y%m%d")
return f"{timestamp}_{file_path.name}"
# Execute parallel rename
renamer.rename_directory_parallel(
"./large_directory",
rename_function=add_timestamp,
pattern="*.txt",
recursive=True,
dry_run=True
)
if __name__ == "__main__":
performance_example()"Parallel processing transforms file renaming from a sequential bottleneck into a high-performance operation that fully utilizes modern multi-core processors, reducing execution time by orders of magnitude for large file collections."
Integration with Other Tools and Workflows
File renaming scripts become even more powerful when integrated into larger automation workflows. Whether you're building a data pipeline, automating media processing, or maintaining a content management system, your renaming utilities can work seamlessly with other tools through APIs, configuration files, and standardized interfaces.
📋 Configuration-Based Renaming
Using configuration files allows non-technical users to define renaming rules without modifying code, making your tools more accessible and maintainable.
import yaml
import json
from pathlib import Path
from typing import Dict, Any
import logging
class ConfigurableRenamer:
"""
File renamer that reads rules from configuration files
"""
def __init__(self, config_file: str):
"""Load configuration from file"""
self.config_file = Path(config_file)
self.config = self.load_config()
self.logger = logging.getLogger(__name__)
def load_config(self) -> Dict[str, Any]:
"""Load configuration from YAML or JSON file"""
if not self.config_file.exists():
raise FileNotFoundError(f"Config file not found: {self.config_file}")
with open(self.config_file, 'r') as f:
if self.config_file.suffix in ['.yaml', '.yml']:
return yaml.safe_load(f)
elif self.config_file.suffix == '.json':
return json.load(f)
else:
raise ValueError(f"Unsupported config format: {self.config_file.suffix}")
def apply_rules(self, dry_run: bool = True):
"""Apply all renaming rules from configuration"""
rules = self.config.get('rules', [])
self.logger.info(f"Loaded {len(rules)} renaming rules from {self.config_file}")
self.logger.info("="*60)
for rule_index, rule in enumerate(rules, 1):
self.logger.info(f"\nApplying rule {rule_index}: {rule.get('name', 'Unnamed')}")
try:
self.apply_single_rule(rule, dry_run)
except Exception as e:
self.logger.error(f"Error applying rule: {str(e)}")
def apply_single_rule(self, rule: Dict[str, Any], dry_run: bool):
"""Apply a single renaming rule"""
# Extract rule parameters
directory = Path(rule['directory'])
action = rule['action']
pattern = rule.get('pattern', '*')
if not directory.exists():
self.logger.warning(f"Directory not found: {directory}")
return
# Get files matching pattern
files = sorted([f for f in directory.glob(pattern) if f.is_file()])
self.logger.info(f" Found {len(files)} files matching pattern '{pattern}'")
# Apply appropriate action
if action == 'sequential':
self.apply_sequential(files, rule, dry_run)
elif action == 'replace':
self.apply_replace(files, rule, dry_run)
elif action == 'prefix':
self.apply_prefix(files, rule, dry_run)
elif action == 'suffix':
self.apply_suffix(files, rule, dry_run)
elif action == 'lowercase':
self.apply_lowercase(files, dry_run)
elif action == 'uppercase':
self.apply_uppercase(files, dry_run)
else:
self.logger.error(f"Unknown action: {action}")
def apply_sequential(self, files: List[Path], rule: Dict, dry_run: bool):
"""Apply sequential numbering"""
prefix = rule.get('prefix', 'file')
start = rule.get('start', 1)
for index, file_path in enumerate(files, start=start):
new_name = f"{prefix}_{index:03d}{file_path.suffix}"
self.rename_file(file_path, new_name, dry_run)
def apply_replace(self, files: List[Path], rule: Dict, dry_run: bool):
"""Apply text replacement"""
find = rule.get('find', '')
replace = rule.get('replace', '')
for file_path in files:
if find in file_path.name:
new_name = file_path.name.replace(find, replace)
self.rename_file(file_path, new_name, dry_run)
def apply_prefix(self, files: List[Path], rule: Dict, dry_run: bool):
"""Add prefix to filenames"""
prefix = rule.get('prefix', '')
for file_path in files:
new_name = f"{prefix}{file_path.name}"
self.rename_file(file_path, new_name, dry_run)
def apply_suffix(self, files: List[Path], rule: Dict, dry_run: bool):
"""Add suffix before extension"""
suffix = rule.get('suffix', '')
for file_path in files:
new_name = f"{file_path.stem}{suffix}{file_path.suffix}"
self.rename_file(file_path, new_name, dry_run)
def apply_lowercase(self, files: List[Path], dry_run: bool):
"""Convert to lowercase"""
for file_path in files:
new_name = file_path.name.lower()
if new_name != file_path.name:
self.rename_file(file_path, new_name, dry_run)
def apply_uppercase(self, files: List[Path], dry_run: bool):
"""Convert to uppercase"""
for file_path in files:
new_name = file_path.name.upper()
if new_name != file_path.name:
self.rename_file(file_path, new_name, dry_run)
def rename_file(self, file_path: Path, new_name: str, dry_run: bool):
"""Perform the actual rename operation"""
new_path = file_path.parent / new_name
if new_path.exists() and new_path != file_path:
self.logger.warning(f" ⚠ Conflict: {new_name} already exists")
return
if dry_run:
self.logger.info(f" Would rename: {file_path.name} → {new_name}")
else:
try:
file_path.rename(new_path)
self.logger.info(f" ✓ Renamed: {file_path.name} → {new_name}")
except Exception as e:
self.logger.error(f" ✗ Error: {file_path.name}: {str(e)}")
# Example configuration file (rename_config.yaml):
"""
rules:
- name: "Organize photos by date"
directory: "./photos"
pattern: "*.jpg"
action: "sequential"
prefix: "photo"
start: 1
- name: "Clean up document names"
directory: "./documents"
pattern: "*.pdf"
action: "replace"
find: " "
replace: "_"
- name: "Add project prefix"
directory: "./code"
pattern: "*.py"
action: "prefix"
prefix: "project_"
- name: "Standardize case"
directory: "./mixed_files"
pattern: "*"
action: "lowercase"
"""
# Example usage
def config_example():
"""Example using configuration file"""
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Create renamer from config
renamer = ConfigurableRenamer("rename_config.yaml")
# Apply all rules in preview mode
renamer.apply_rules(dry_run=True)
# After review, execute
# renamer.apply_rules(dry_run=False)
if __name__ == "__main__":
config_example()How do I prevent accidentally overwriting files when renaming?
Always implement conflict detection by checking if the target filename already exists before renaming. Use the dry-run mode to preview all changes, create backups before executing operations, and implement rollback capabilities. The FileRenameManager class demonstrated in this guide includes all these safety mechanisms, ensuring you can review changes before committing them and restore the original state if needed.
Can I undo file renaming operations after they've been executed?
Yes, if you've implemented proper logging and transaction tracking. The FileRenameManager class saves a detailed log of all operations including original and new filenames. You can create a rollback function that reads this log and reverses each operation. Additionally, creating a backup directory before renaming provides a complete safety net that allows you to restore the entire original state regardless of what changes were made.
How can I rename files based on their content rather than just filename patterns?
You can read file content and extract metadata to inform renaming decisions. For images, use the PIL library to read EXIF data containing camera information and timestamps. For documents, use libraries like PyPDF2 or python-docx to extract document properties. For media files, use libraries like mutagen to read audio/video metadata. This metadata can then be incorporated into your filename generation logic to create descriptive, content-based names.
What's the best way to handle files with special characters or Unicode names?
Python 3's pathlib module handles Unicode filenames natively across different operating systems. To sanitize problematic characters, use regular expressions to identify and replace illegal characters specific to your target filesystem. The comprehensive_rename function demonstrated in this guide shows how to remove special characters while preserving readability. Always test your renaming logic on a small sample of files with various character sets before processing large batches.
How do I rename files across multiple directories while maintaining folder structure?
Use recursive directory traversal with pathlib's rglob() method or os.walk() to process files in all subdirectories. When renaming, preserve the directory portion of the path while only modifying the filename component. The rename_recursive function shown in this guide demonstrates how to process entire directory trees while applying consistent renaming rules at any depth, with options to exclude specific directories and limit recursion depth.
Can I schedule file renaming scripts to run automatically at specific times?
Yes, use your operating system's task scheduler. On Linux/macOS, use cron jobs by editing your crontab file. On Windows, use Task Scheduler to create scheduled tasks. Make your Python script executable, ensure it uses absolute paths rather than relative paths, and configure proper logging so you can monitor automated runs. The command-line tool structure demonstrated in this guide makes scripts easy to schedule since they accept all parameters as arguments rather than requiring code modifications.