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

TopicWhat it covers
Opening, inspecting and saving imagesImage.open reads the header only. Pixels are decoded when you first use them, which is why the returned object is cheaplesson
Installing Pillow and the format matrixThe import name is still PIL for historical reasons, but the package is pillow. If PIL.__file__ points somewherelesson
Bands, channels and colour modes in depthsplit() returns one image per band, each in the corresponding single-band mode. merge() requires exactly matching sizeslesson
Filters and enhancementBlur, sharpen and detect edges with ImageFilter, tune brightness and contrast with ImageEnhance, and understand whatlesson
ImageOps: autocontrast, pad, fit and montageImageOps.fit takes a centering tuple in the range 0 to 1. The default (0.5, 0.5) centres the crop, which is often wronglesson
Reading and writing EXIF and image metadataexif_transpose is the correct fix. Rotating the pixels yourself leaves the orientation tag in place, so a viewer thatlesson
Animated images: GIF and WebP framesIterate frames of an animation, assemble your own, control duration and looping, and work around the palette limits oflesson
Batch image processing scripts and performanceThe with block matters: Pillow keeps the file handle open lazily, and a long batch without closing descriptors will hitlesson
Debugging Pillow: mode errors, bombs and truncated filesDiagnose the exceptions Pillow actually raises, disable the decompression-bomb guard deliberately, and handle damagedlesson

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_profile

Full 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, bounded

Full 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?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 9 lessons of the Pillow course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Pillow course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Python 3 NumPy pandas Matplotlib Jupyter Notebook Flask

Last refreshed 2026-09-27.