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.
Search across all documentation pages
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.
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:
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:
exif_transpose fixes rotated phone photosconvert("RGB") normalizes modes before WEBP exportBytesIO pipeline for API handlersRGB, RGBA, L (grayscale), P (palette) - convert before formats that do not support alpha.resize, thumbnail, crop, rotate, filter module for blur/sharpen.format= kwarg; quality settings vary per codec.| 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(...) |
# Verify dimensions without full decode (where supported)
with Image.open(path) as img:
width, height = img.sizeImageOps.exif_transpose.convert("RGB") on white background first.img.format after open.| 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 |
Requires pillow-heif plugin - otherwise convert on client or reject with clear error.
Keep RGBA mode; do not convert to RGB before save.
Yes with slim layer builds - watch package size and native lib dependencies.
Use Image.alpha_composite with a transparent PNG watermark layer.
Image.Resampling.LANCZOS for downscale; NEAREST for pixel art.
Strip location EXIF on user uploads before publishing - privacy requirement.
Recent Pillow versions add AVIF if libavif is present - verify in target deploy image.
Generate solid-color 10x10 PNG in tests via Image.new("RGB", (10, 10), "red").
Do not share mutating Image objects across threads - open per task.
data = await file.read() then pass bytes to BytesIO pipeline with size limits.
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+.
Reviewed by Chris St. John·Last updated Jul 19, 2026