Pillow cheat sheet
A scannable Pillow reference: 13 short snippets across 9 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Opening, inspecting and saving images | Image.open reads the header only. Pixels are decoded when you first use them, which is why the returned object is cheap | lesson |
| Installing Pillow and the format matrix | The import name is still PIL for historical reasons, but the package is pillow. If PIL.__file__ points somewhere | lesson |
| Bands, channels and colour modes in depth | split() returns one image per band, each in the corresponding single-band mode. merge() requires exactly matching sizes | lesson |
| Filters and enhancement | Blur, sharpen and detect edges with ImageFilter, tune brightness and contrast with ImageEnhance, and understand what | lesson |
| ImageOps: autocontrast, pad, fit and montage | ImageOps.fit takes a centering tuple in the range 0 to 1. The default (0.5, 0.5) centres the crop, which is often wrong | lesson |
| Reading and writing EXIF and image metadata | exif_transpose is the correct fix. Rotating the pixels yourself leaves the orientation tag in place, so a viewer that | lesson |
| Animated images: GIF and WebP frames | Iterate frames of an animation, assemble your own, control duration and looping, and work around the palette limits of | lesson |
| Batch image processing scripts and performance | The with block matters: Pillow keeps the file handle open lazily, and a long batch without closing descriptors will hit | lesson |
| Debugging Pillow: mode errors, bombs and truncated files | Diagnose the exceptions Pillow actually raises, disable the decompression-bomb guard deliberately, and handle damaged | lesson |
Quick snippets
Opening, inspecting and saving images
Saving and format support
from PIL import Image
im = Image.open("photo.jpg").convert("RGB")
# the format comes from the extension when you pass a path
im.save("out.jpg", quality=85, optimize=True, progressive=True)
im.save("out.webp", quality=80, method=6) # method 0-6: slower means smaller
im.save("out.png", optimize=True) # PNG is lossless; quality is ignored
# for a file object you must state the format explicitly
with open("stream.jpg", "wb") as fh:
im.save(fh, format="JPEG", quality=85)Full lesson: Opening, inspecting and saving images →
Installing Pillow and the format matrix
Install the right build
pip install --upgrade Pillow
# confirm it is real Pillow, not the abandoned PIL
python -c "import PIL; print(PIL.__version__, PIL.__file__)"
# which optional codecs did this build get?
python -c "from PIL import features; features.pilinfo()"Full lesson: Installing Pillow and the format matrix →
Bands, channels and colour modes in depth
The mode table
from PIL import Image
im = Image.open("photo.png")
print(im.mode, im.getbands(), im.getextrema())
# split a colour image into channels and reassemble
r, g, b = im.convert("RGB").split()
red_only = Image.merge("RGB", (r, Image.new("L", im.size, 0), Image.new("L", im.size, 0)))
# work on one channel without touching the others
g = g.point(lambda v: min(255, int(v * 1.2)))
im2 = Image.merge("RGB", (r, g, b))
Mode and format must agree
# JPEG cannot store alpha: flatten onto a background first
if im.mode in ("RGBA", "LA", "P"):
im = im.convert("RGBA")
bg = Image.new("RGB", im.size, "white")
bg.paste(im, mask=im.split()[-1])
im = bg
im.save("out.jpg", quality=85, optimize=True)Full lesson: Bands, channels and colour modes in depth →
Filters and enhancement
ImageFilter
from PIL import Image, ImageFilter
im = Image.open("photo.jpg").convert("RGB")
soft = im.filter(ImageFilter.GaussianBlur(radius=2))
sharp = im.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
edges = im.filter(ImageFilter.FIND_EDGES)
clean = im.filter(ImageFilter.MedianFilter(size=5)) # salt-and-pepper noise
# a custom 3x3 kernel: a simple emboss
emboss = ImageFilter.Kernel((3, 3), (-2, -1, 0, -1, 1, 1, 0, 1, 2), scale=1, offset=128)
out = im.filter(emboss)
ImageEnhance
from PIL import ImageEnhance
im = Image.open("photo.jpg").convert("RGB")
im = ImageEnhance.Brightness(im).enhance(1.15) # 1.0 is unchanged
im = ImageEnhance.Contrast(im).enhance(1.2)
im = ImageEnhance.Color(im).enhance(0.9) # saturation
im = ImageEnhance.Sharpness(im).enhance(1.5)
# chaining reads top to bottom because each call returns a new image
im.save("enhanced.jpg", quality=88)
Cost and where to filter
# filter the small version when the output is small
thumb = im.copy()
thumb.thumbnail((800, 800), Image.Resampling.LANCZOS)
thumb = thumb.filter(ImageFilter.GaussianBlur(radius=1.5))
# or draft a JPEG down before doing anything expensive
import io
small = Image.open("huge.jpg")
small.draft("RGB", (1600, 1600)) # decodes at a reduced scale
small = small.filter(ImageFilter.UnsharpMask(radius=1, percent=120))Full lesson: Filters and enhancement →
ImageOps: autocontrast, pad, fit and montage
Tone operations
from PIL import Image, ImageOps
im = Image.open("scan.png").convert("RGB")
im = ImageOps.autocontrast(im, cutoff=1) # stretch to full range, ignoring 1% tails
im = ImageOps.equalize(im) # flatten the histogram
im = ImageOps.posterize(im, bits=4) # 16 levels per channel, a poster look
im = ImageOps.solarize(im, threshold=128) # invert values above the threshold
grey = ImageOps.grayscale(im) # luminance conversion
tinted = ImageOps.colorize(grey, black="navy", white="#fffbe6")
print(tinted.mode, ImageOps.invert(tinted.convert("RGB")).size)Full lesson: ImageOps: autocontrast, pad, fit and montage →
Reading and writing EXIF and image metadata
Writing and stripping
# carry EXIF forward when converting
im = Image.open("phone.jpg")
icc = im.info.get("icc_profile")
exif_bytes = im.getexif().tobytes()
im.save("out.jpg", quality=90, exif=exif_bytes, icc_profile=icc)
# strip everything before publishing
clean = Image.open("phone.jpg")
data = list(clean.getdata())
stripped = Image.new(clean.mode, clean.size)
stripped.putdata(data)
stripped.save("published.jpg", quality=90) # no exif, no icc_profileFull lesson: Reading and writing EXIF and image metadata →
Animated images: GIF and WebP frames
Pitfalls
# frames must share a size: normalise before saving
target = frames[0].size
frames = [f if f.size == target else f.resize(target, Image.Resampling.LANCZOS) for f in frames]
# disposal=2 clears each frame to the background before the next one draws
frames[0].save("clean.gif", save_all=True, append_images=frames[1:],
duration=100, loop=0, disposal=2)Full lesson: Animated images: GIF and WebP frames →
Batch image processing scripts and performance
Decoding less
with Image.open("huge.jpg") as im:
im.draft("RGB", (1200, 1200)) # decode at a reduced scale: 4x to 64x less work
im = im.resize((1200, int(1200 * im.height / im.width)), Image.Resampling.LANCZOS)
im.save("thumb.jpg", quality=85, optimize=True)
# Pillow's own decoder threads help for PNG and are ignored for others
from PIL import Image as I
I.MAX_IMAGE_PIXELS = 200_000_000 # allow larger inputs, boundedFull lesson: Batch image processing scripts and performance →
Debugging Pillow: mode errors, bombs and truncated files
The exceptions you will meet
from PIL import Image, UnidentifiedImageError
try:
im = Image.open(path)
im.load() # forces the decode, so failures happen here
except UnidentifiedImageError:
print("not an image:", path)
except OSError as exc:
print("damaged or unsupported:", path, exc)
else:
print(im.format, im.mode, im.size)
verify() and the reopen rule
# verify() checks the header only, then leaves the object unusable
with Image.open(path) as probe:
try:
probe.verify()
except Exception as exc:
print("header damaged:", exc)
# check the pixels with a fresh open
with Image.open(path) as im:
im.load()
print(im.size)Full lesson: Debugging Pillow: mode errors, bombs and truncated files →
FAQ
Is this Pillow cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Python 3 NumPy pandas Matplotlib Jupyter Notebook Flask
Last refreshed 2026-09-27.