If you're building a website in 2026, image optimization isn't optional—it's essential. Images often account for 60-80% of a webpage's total size, and unoptimized images can destroy your site's performance, SEO rankings, and user experience. The solution? WebP format combined with smart Python automation.
This guide will show you exactly how to optimize all your PNG and JPG images to WebP format using Python scripts. By the end, you'll have a complete, production-ready solution that automatically converts, optimizes, and updates your website assets.
Why WebP? The 2026 Standard
WebP is Google's modern image format that provides superior compression compared to PNG and JPEG. Here's what you're getting:
- 25-35% smaller than JPEG at the same quality level
- 26% smaller than PNG with lossless compression
- Native browser support in all modern browsers (96%+ coverage)
- Progressive loading for better perceived performance
- Transparency support like PNG, with better compression
In 2026, using WebP isn't cutting-edge—it's the baseline. Google's PageSpeed Insights penalizes sites using old formats, and Core Web Vitals metrics heavily favor optimized images. Your competitors are already using WebP. Don't fall behind.
The Complete Python Solution
Here's a production-ready Python script that handles everything: conversion, optimization, error handling, and HTML updates.
Save this as convert_to_webp.py:
#!/usr/bin/env python3
"""
WebP Image Converter and Optimizer
Converts PNG and JPG images to WebP format for optimal website performance.
Requirements: pip install pillow
Usage: python convert_to_webp.py [image_directory]
"""
import os
import sys
from pathlib import Path
from PIL import Image
import argparse
def convert_to_webp(input_path, output_path=None, quality=85, lossless=False):
"""
Convert an image to WebP format.
Args:
input_path: Path to source image
output_path: Destination path (default: same name with .webp extension)
quality: Quality for lossy compression (1-100, default 85)
lossless: Use lossless compression (ignores quality setting)
Returns:
Tuple of (success: bool, original_size: int, new_size: int, savings: float)
"""
try:
# Open and validate image
with Image.open(input_path) as img:
# Convert RGBA images properly
if img.mode in ('RGBA', 'LA', 'P'):
# Preserve transparency
img = img.convert('RGBA')
lossless = True # Use lossless for images with transparency
elif img.mode not in ('RGB', 'L'):
img = img.convert('RGB')
# Determine output path
if output_path is None:
output_path = Path(input_path).with_suffix('.webp')
else:
output_path = Path(output_path)
# Get original file size
original_size = os.path.getsize(input_path)
# Convert to WebP
save_kwargs = {
'format': 'WebP',
'method': 6, # Best compression method
}
if lossless:
save_kwargs['lossless'] = True
else:
save_kwargs['quality'] = quality
save_kwargs['method'] = 6
img.save(output_path, **save_kwargs)
# Get new file size
new_size = os.path.getsize(output_path)
savings = ((original_size - new_size) / original_size) * 100
return True, original_size, new_size, savings
except Exception as e:
print(f"Error converting {input_path}: {str(e)}", file=sys.stderr)
return False, 0, 0, 0.0
def process_directory(directory, quality=85, recursive=True, delete_originals=False):
"""
Process all PNG and JPG images in a directory.
Args:
directory: Directory path to process
quality: WebP quality setting (1-100)
recursive: Process subdirectories
delete_originals: Delete original files after conversion
Returns:
Statistics dictionary
"""
directory = Path(directory)
if not directory.exists():
print(f"Error: Directory '{directory}' does not exist", file=sys.stderr)
return None
# Find all image files
extensions = {'.png', '.jpg', '.jpeg'}
if recursive:
image_files = [
f for ext in extensions
for f in directory.rglob(f'*{ext}')
if f.is_file() and not f.suffix.lower() == '.webp'
]
else:
image_files = [
f for ext in extensions
for f in directory.glob(f'*{ext}')
if f.is_file() and not f.suffix.lower() == '.webp'
]
if not image_files:
print(f"No PNG or JPG images found in '{directory}'")
return None
# Process images
stats = {
'total': len(image_files),
'converted': 0,
'skipped': 0,
'errors': 0,
'total_original_size': 0,
'total_new_size': 0,
'total_savings': 0.0
}
print(f"Found {stats['total']} images to process...")
print(f"{'='*60}")
for i, img_path in enumerate(image_files, 1):
webp_path = img_path.with_suffix('.webp')
# Skip if WebP already exists and is newer
if webp_path.exists():
if webp_path.stat().st_mtime > img_path.stat().st_mtime:
print(f"[{i}/{stats['total']}] ✓ Skipped (already exists): {img_path.name}")
stats['skipped'] += 1
continue
# Determine if image has transparency
try:
with Image.open(img_path) as img:
has_transparency = img.mode in ('RGBA', 'LA') or 'transparency' in img.info
except:
has_transparency = False
# Convert to WebP
success, orig_size, new_size, savings = convert_to_webp(
img_path,
webp_path,
quality=quality,
lossless=has_transparency
)
if success:
stats['converted'] += 1
stats['total_original_size'] += orig_size
stats['total_new_size'] += new_size
# Format sizes for display
orig_mb = orig_size / (1024 * 1024)
new_mb = new_size / (1024 * 1024)
print(f"[{i}/{stats['total']}] ✓ Converted: {img_path.name}")
print(f" {orig_mb:.2f} MB → {new_mb:.2f} MB ({savings:.1f}% smaller)")
# Delete original if requested
if delete_originals:
try:
img_path.unlink()
print(f" Deleted original: {img_path.name}")
except Exception as e:
print(f" Warning: Could not delete {img_path.name}: {e}", file=sys.stderr)
else:
stats['errors'] += 1
print(f"[{i}/{stats['total']}] ✗ Failed: {img_path.name}")
# Print summary
print(f"{'='*60}")
print("Conversion Summary:")
print(f" Total images: {stats['total']}")
print(f" Converted: {stats['converted']}")
print(f" Skipped: {stats['skipped']}")
print(f" Errors: {stats['errors']}")
if stats['converted'] > 0:
total_orig_mb = stats['total_original_size'] / (1024 * 1024)
total_new_mb = stats['total_new_size'] / (1024 * 1024)
total_savings = ((stats['total_original_size'] - stats['total_new_size']) / stats['total_original_size']) * 100
print(f"\n Original total size: {total_orig_mb:.2f} MB")
print(f" New total size: {total_new_mb:.2f} MB")
print(f" Total space saved: {total_savings:.1f}% ({total_orig_mb - total_new_mb:.2f} MB)")
return stats
def main():
parser = argparse.ArgumentParser(
description='Convert PNG and JPG images to WebP format',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
python convert_to_webp.py images/
python convert_to_webp.py images/ --quality 90
python convert_to_webp.py images/ --recursive --delete-originals
'''
)
parser.add_argument('directory', help='Directory containing images to convert')
parser.add_argument('--quality', type=int, default=85, choices=range(1, 101),
help='WebP quality (1-100, default: 85)')
parser.add_argument('--recursive', '-r', action='store_true',
help='Process subdirectories recursively')
parser.add_argument('--delete-originals', '-d', action='store_true',
help='Delete original files after conversion (USE WITH CAUTION)')
args = parser.parse_args()
# Check for Pillow
try:
from PIL import Image
except ImportError:
print("Error: Pillow library not found.", file=sys.stderr)
print("Install it with: pip install pillow", file=sys.stderr)
sys.exit(1)
# Process directory
stats = process_directory(
args.directory,
quality=args.quality,
recursive=args.recursive,
delete_originals=args.delete_originals
)
if stats and stats['errors'] > 0:
sys.exit(1)
if __name__ == '__main__':
main()
Updating HTML Files Automatically
After converting images, you need to update your HTML files to reference the new WebP images. Here's a Python script that handles this intelligently:
#!/usr/bin/env python3
"""
Update HTML files to use WebP images with fallbacks.
Replaces image references and adds picture elements for maximum compatibility.
"""
import os
import re
from pathlib import Path
from html.parser import HTMLParser
from html import escape
def update_html_images(html_path, images_directory='images'):
"""
Update HTML file to use WebP images with proper fallbacks.
Args:
html_path: Path to HTML file
images_directory: Relative path to images directory
"""
try:
with open(html_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f"Error reading {html_path}: {e}")
return False
original_content = content
images_dir = Path(html_path).parent / images_directory
# Pattern to match img src attributes
img_pattern = r'<img([^>]*?)src=["\']([^"\']+\.(jpg|jpeg|png))["\']([^>]*?)>'
def replace_img(match):
full_tag = match.group(0)
before_src = match.group(1)
img_path = match.group(2)
extension = match.group(3)
after_src = match.group(4)
# Skip if already a picture element or WebP
if 'picture' in full_tag.lower() or '.webp' in img_path.lower():
return full_tag
# Check if WebP version exists
webp_path = Path(img_path).with_suffix('.webp')
webp_full_path = images_dir / webp_path.name if not Path(img_path).is_absolute() else Path(img_path).with_suffix('.webp')
if webp_full_path.exists():
# Create picture element with fallback
alt_match = re.search(r'alt=["\']([^"\']*)["\']', full_tag)
alt_text = alt_match.group(1) if alt_match else ''
picture_html = f'''<picture>
<source srcset="{webp_path}" type="image/webp">
<img{before_src}src="{img_path}"{after_src}>
</picture>'''
return picture_html
return full_tag
# Replace img tags with picture elements
new_content = re.sub(img_pattern, replace_img, content, flags=re.IGNORECASE)
# Only write if content changed
if new_content != original_content:
try:
with open(html_path, 'w', encoding='utf-8') as f:
f.write(new_content)
return True
except Exception as e:
print(f"Error writing {html_path}: {e}")
return False
return False
def process_html_files(directory, images_directory='images', recursive=True):
"""
Process all HTML files in a directory.
Args:
directory: Directory containing HTML files
images_directory: Relative path to images directory
recursive: Process subdirectories
"""
directory = Path(directory)
if recursive:
html_files = list(directory.rglob('*.html'))
else:
html_files = list(directory.glob('*.html'))
if not html_files:
print(f"No HTML files found in '{directory}'")
return
print(f"Found {len(html_files)} HTML files to process...")
updated = 0
for html_file in html_files:
if update_html_images(html_file, images_directory):
print(f"✓ Updated: {html_file.name}")
updated += 1
else:
print(f" Skipped: {html_file.name} (no changes needed)")
print(f"\nUpdated {updated} out of {len(html_files)} HTML files")
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Update HTML files to use WebP images')
parser.add_argument('directory', help='Directory containing HTML files')
parser.add_argument('--images-dir', default='images', help='Images directory path (default: images)')
parser.add_argument('--recursive', '-r', action='store_true', help='Process subdirectories')
args = parser.parse_args()
process_html_files(args.directory, args.images_dir, args.recursive)
Complete Workflow: Step by Step
- Install Pillow:
pip install pillow - Save the conversion script as
convert_to_webp.py - Run the conversion:
python convert_to_webp.py images/ - Update HTML files: Save the HTML updater script and run it
- Test your website: Verify images load correctly with proper fallbacks
Best Practices for 2026
- Quality Settings: Use 85-90 for photos, lossless for graphics/logos
- Always Provide Fallbacks: Not all browsers support WebP (though coverage is 96%+)
- Automate Everything: Add scripts to your build process or CI/CD pipeline
- Monitor Results: Check PageSpeed Insights before and after optimization
- Progressive Enhancement: Use <picture> elements for maximum compatibility
Expected Results
After running these scripts, expect:
- 30-50% reduction in total image file sizes
- Faster load times and improved Core Web Vitals scores
- Better SEO rankings from improved PageSpeed scores
- Reduced bandwidth costs and better mobile experience
Real-World Impact
I've seen websites reduce their total page weight by 60%+ just by converting images to WebP. On a typical e-commerce site with 100 images, this can mean the difference between a 4-second load time and a sub-2-second load time. That's the difference between losing customers and keeping them.
⚡ Quick Start Command
# Convert all images in your images/ directory
python convert_to_webp.py images/ --recursive
# Update all HTML files to use WebP with fallbacks
python update_html_webp.py . --images-dir images --recursive
Conclusion: Building the Best Website in 2026
Image optimization with WebP isn't a nice-to-have in 2026—it's a requirement. Users expect fast websites, search engines rank fast sites higher, and hosting costs decrease when you serve smaller files.
These Python scripts give you a complete, automated solution. Run them once, integrate them into your workflow, and watch your website performance metrics improve. In 2026, this is how you build websites that actually perform.