Pillow
Pillow is the maintained fork of PIL - the standard library for opening, converting, resizing, cropping, and saving images in Python pipelines, web upload handlers, and ML preprocessing.
Recipe
from PIL import Image
with Image.open("photo.jpg") as img:
img = ImageOps.exif_transpose(img)
img.thumbnail((800, 800))
img.save("preview.webp", format="WEBP", quality=85)When to reach for this:
- User avatar/thumbnail generation in web apps
- Batch converting PNG/JPEG/WEBP for CDNs
- Preprocessing images before PyTorch/torchvision training
- Reading dimensions and metadata without loading full decode into UI
Working Example
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from PIL import Image, ImageOps, UnidentifiedImageError
MAX_BYTES = 5 * 1024 * 1024
ALLOWED = {"JPEG", "PNG", "WEBP"}
def make_thumbnail(data: bytes, max_edge: int = 256) -> bytes:
if len(data) > MAX_BYTES:
raise ValueError("file too large")
try:
with Image.open(BytesIO(data)) as img:
if img.format not in ALLOWED:
raise ValueError(f"unsupported format {img.format}")
img = ImageOps.exif_transpose(img)
img = img.convert("RGB")
img.thumbnail((max_edge, max_edge))
out = BytesIO()
img.save(out, format="WEBP", quality=80, method=6)
return out.getvalue()
except UnidentifiedImageError as exc:
raise ValueError("not an image") from exc
if __name__ == "__main__":
sample = Path("photo.jpg").read_bytes()
Path("thumb.webp").write_bytes(make_thumbnail(sample))What this demonstrates:
- Size cap before decode mitigates decompression bombs
exif_transposefixes rotated phone photosconvert("RGB")normalizes modes before WEBP export- In-memory
BytesIOpipeline for API handlers - Explicit allowed formats whitelist
Deep Dive
How It Works
- Image.open - Lazy load; decode happens on first pixel access.
- Modes -
RGB,RGBA,L(grayscale),P(palette) - convert before formats that do not support alpha. - Transforms -
resize,thumbnail,crop,rotate,filtermodule for blur/sharpen. - Saving - Format from extension or
format=kwarg; quality settings vary per codec.
Common Operations
| Task | API |
|---|---|
| Max bounding box | thumbnail((w, h)) |
| Exact dimensions | resize((w, h), Image.Resampling.LANCZOS) |
| Strip EXIF | img.getexif().clear() or save without exif |
| Draw text | ImageDraw.Draw(img).text(...) |
Python Notes
# Verify dimensions without full decode (where supported)
with Image.open(path) as img:
width, height = img.sizeGotchas
- Decompression bombs - Tiny PNG expands to gigapixel. Fix: max bytes + max dimension checks pre-decode.
- Ignoring EXIF orientation - Thumbnails appear sideways. Fix:
ImageOps.exif_transpose. - RGBA to JPEG - Save fails or drops alpha oddly. Fix:
convert("RGB")on white background first. - Not closing files on Windows - File lock on overwrite. Fix: context managers.
- Trusting client Content-Type - Attackers spoof MIME. Fix: validate
img.formatafter open. - LANCZOS on huge images - Memory spikes. Fix: thumbnail first, then optional resize.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
wand (ImageMagick) | Complex PDF/PSD/SVG rasterization | Simpler deps (ImageMagick binary required) |
opencv-python | CV pipelines, face detection | Simple thumbnail microservice |
torchvision.transforms | Training-time aug only | General upload sanitization |
| SaaS image CDN | Offload transforms at edge | Air-gapped or cost-sensitive workloads |
FAQs
Does Pillow read HEIC iPhone photos?
Requires pillow-heif plugin - otherwise convert on client or reject with clear error.
How do I preserve transparency in PNG?
Keep RGBA mode; do not convert to RGB before save.
Can Pillow run in AWS Lambda?
Yes with slim layer builds - watch package size and native lib dependencies.
How do I watermark images?
Use Image.alpha_composite with a transparent PNG watermark layer.
What resampling filter for photos?
Image.Resampling.LANCZOS for downscale; NEAREST for pixel art.
How do I read GPS EXIF safely?
Strip location EXIF on user uploads before publishing - privacy requirement.
Does Pillow support AVIF?
Recent Pillow versions add AVIF if libavif is present - verify in target deploy image.
How do I test without image fixtures?
Generate solid-color 10x10 PNG in tests via Image.new("RGB", (10, 10), "red").
Thread-safe?
Do not share mutating Image objects across threads - open per task.
Integrate with FastAPI UploadFile?
data = await file.read() then pass bytes to BytesIO pipeline with size limits.
Related
- PyTorch / Deep Learning Skill - training image pipelines
- Input Validation - upload security
- FastAPI Basics - file upload routes
- File and Directory Automation - batch processing
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.