Why I Chose Pillow to Handle the Tedious Tasks
Python has long been my ‘right hand’ for handling repetitive tasks, from running deployment scripts to receiving server alerts. I once struggled with a massive library of over 50,000 images for an e-commerce platform. Manually opening each file in Photoshop to resize and add a logo was a total nightmare. After six months of letting the script ‘grind’ in production, I can confirm that Pillow (a PIL fork) is the most stable library for handling this kind of mess.
This library is not only incredibly fast at reading and writing files but also runs extremely smoothly on Linux servers. Here are the ‘tricks’ I’ve used to keep the system running flawlessly for the past six months.
Quick Start: Basic Image Processing in Seconds
First, let’s get the library installed. Open your terminal and type:
pip install Pillow
Here is a script you can run immediately to ‘shrink’ any image:
from PIL import Image
# Open image file
with Image.open("input.jpg") as img:
# Force resize to 800x600
resized_img = img.resize((800, 600))
resized_img.save("output.jpg")
print("Done! Fast and furious.")
It looks simple, but to make the script run ‘smoothly’ on a production server without ruining the images, you need to pay attention to a few technical details below.
Explaining ‘Hard-Won’ Real-World Techniques
1. Resizing Without Distortion (Aspect Ratio)
If you use the resize() function and pass in hard-coded numbers, the image can easily end up looking ‘squashed’ or ‘stretched’ due to an incorrect aspect ratio. Believe me, clients won’t like that. Instead, I always prioritize using the thumbnail() method.
This function resizes the image in place and automatically calculates the dimensions so the image fits within the frame while maintaining its original aspect ratio:
with Image.open("landscape.jpg") as img:
max_size = (1200, 1200)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
img.save("landscape_optimized.jpg")
Small note: Always use Image.Resampling.LANCZOS. Even though it takes a few extra milliseconds to process, the output quality is significantly sharper compared to ‘cheap’ filters like NEAREST.
2. Adding Watermarks: Batch Copyright Protection
Inserting a logo while maintaining transparency requires an understanding of the Alpha channel. The code below is calculated so the logo always sits in the bottom-right corner, with a fixed margin, regardless of how large the original image is.
def add_watermark(main_image_path, watermark_path, output_path):
main_img = Image.open(main_image_path).convert("RGBA")
watermark = Image.open(watermark_path).convert("RGBA")
# Resize logo to occupy about 20% of the main image width
w_width, w_height = watermark.size
main_width, main_height = main_img.size
new_width = int(main_width * 0.2)
new_height = int(w_height * (new_width / w_width))
watermark = watermark.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Bottom-right position, 20px margin
position = (main_width - new_width - 20, main_height - new_height - 20)
overlay = Image.new("RGBA", main_img.size, (0, 0, 0, 0))
overlay.paste(watermark, position)
# Merge layers and save file
combined = Image.alpha_composite(main_img, overlay)
combined.convert("RGB").save(output_path, "JPEG", quality=90)
Using alpha_composite ensures the logo’s edges look smooth, avoiding the annoying aliasing (jagged edges) you might get with the standard paste function.
3. ‘Slimming Down’ Images for SEO Optimization
Heavy image files are SEO killers and a waste of bandwidth. A project I worked on reduced total storage from 200GB to less than 60GB by applying these three rules:
- Quality 85: This is the ‘sweet spot.’ File size drops by 70%, but the human eye can barely see the difference.
- Optimize=True: Forces Pillow to scan the color palette to compress it one more time before saving.
- Switch to WebP: This format makes images 30-50% lighter than JPEGs while maintaining sharpness.
# Deep compression using WebP format
img.save("optimized.webp", "WEBP", quality=80, method=6)
Batch Processing
In reality, nobody runs a script for a single file. I usually combine Pillow with pathlib to scan an entire image directory in seconds:
from pathlib import Path
def process_all_images(input_dir, output_dir):
path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for img_file in path.glob("*.jpg"):
try:
with Image.open(img_file) as img:
# Perform processing steps...
target = output_path / f"done_{img_file.name}"
img.save(target, quality=85, optimize=True)
print(f"Done: {img_file.name}")
except Exception as e:
print(f"Error processing file {img_file}: {e}")
Common Pitfalls to Avoid
After half a year of ‘maintaining’ this tool on a server, I’ve gathered three critical lessons:
- Rotation Errors (EXIF): Photos taken on an iPhone often appear sideways when opened with Pillow. Use
ImageOps.exif_transposeto fix this immediately. - Be Careful with RAM: Processing images over 10,000px will eat up all your RAM. Always use the
withstatement to close files as soon as you’re done. - Forgetting Color Conversion: Never save transparent images (RGBA) directly to JPEG unless you want the script to crash. Always
convert("RGB")before saving as a JPEG.
In summary, automating image processing isn’t difficult. With just a few lines of Python code and the Pillow library, you can build a professional system, saving millions in costs compared to third-party SaaS services.

