cheat sheet

Pillow

Package-level reference for Pillow on PyPI — install variants, format-specific native deps, version policy, and alternatives.

Pillow

What it is

Pillow is the actively maintained fork of the original Python Imaging Library (PIL), which was last released in 2009. Forked in 2010 by Alex Clark and now stewarded by a community team, Pillow is the de-facto standard image I/O and pixel-manipulation library on PyPI. Almost every Python web framework, data-science library, ML preprocessor, and visualization tool reads images through Pillow.

Reach for Pillow when you need format support (JPEG, PNG, GIF, TIFF, WebP, BMP, ICO, and 25+ more), simple transforms (resize, crop, rotate, paste), or color-mode conversions. Reach for OpenCV when you need computer-vision algorithms; for wand if you want ImageMagick semantics; for imageio for video and scientific stacks.

Install

bash
pip install pillow

Output: (none — exits 0 on success)

bash
uv add pillow

Output: resolved + added to pyproject.toml

bash
poetry add pillow

Output: updated lockfile + virtualenv install

bash
conda install -c conda-forge pillow

Output: installs Pillow with a fully bundled libjpeg/libpng/libtiff/libwebp stack — preferred when wheels lag on a new Python.

Versioning & Python support

  • Current stable line is the 11.x series (as of late 2025). Pillow follows a roughly quarterly minor-release cadence.
  • Supports Python 3.9+ on recent releases. Older Pillow releases support 3.8 and earlier; the EOL policy matches CPython.
  • Loose semver — major bumps drop EOL Python versions and may remove APIs deprecated in two prior majors.
  • The deprecated PIL name is retained as the import path (from PIL import Image). The PyPI distribution name is pillow.

Package metadata

  • Maintainer: Pillow contributors (Andrew Murray et al.)
  • Project home: github.com/python-pillow/Pillow
  • Docs: pillow.readthedocs.io
  • PyPI: pypi.org/project/pillow
  • License: HPND (Historical Permission Notice and Disclaimer — MIT-compatible)
  • Governance: Tidelift-supported, community-maintained
  • First released: 2010 (fork); PIL itself dates to 1995
  • Downloads: hundreds of millions per month — consistently in the PyPI top 20

Optional dependencies & extras

Pillow has no extras_require markers — the wheel ships with bundled copies of the most common native codecs. Format support depends on what was compiled in at wheel-build time:

  • JPEG — libjpeg (or libjpeg-turbo). Bundled in every official wheel.
  • PNG — zlib. Bundled.
  • TIFF / multi-page TIFF — libtiff. Bundled, but multi-page LZW and JPEG-inside-TIFF require recent libtiff.
  • WebP / WebP-animated — libwebp. Bundled in wheels since ~8.0; missing in some minimal-Linux source builds.
  • HEIF / HEICnot included. Install the separate pillow-heif package and register: pillow_heif.register_heif_opener().
  • AVIF — limited; install pillow-avif-plugin for full read/write.
  • JPEG2000 — needs libopenjp2 at wheel build time; ARM Linux wheels sometimes lack it.
  • FreeType (text rendering) — bundled; lets ImageFont.truetype load TTF/OTF files.
  • LittleCMS (color management) — bundled; required for ImageCms ICC-profile work.

For an exhaustive list of compiled-in features, run:

python
from PIL import features
features.pilinfo()

Output:

text
Pillow 11.x.y
Python ...
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.x.y
--- JPEG support ok, loaded ...
--- PNG support ok, loaded ...
... (truncated)

Alternatives

PackageTrade-off
opencv-pythonImage + video + CV algorithms. Use for computer vision; much heavier dependency tree.
imageioRead/write 50+ formats including video and scientific. Use when you need uniform imread/imwrite across image and video.
wandPython binding for ImageMagick. Use when you want MagickWand semantics or already use ImageMagick CLI elsewhere.
scikit-imageImage-processing algorithms on top of NumPy arrays. Use for filters, segmentation, morphology; not for I/O.
pillow-simdDrop-in Pillow fork with SIMD-accelerated resize/blur. Use only on Intel x86_64 with a compatible CPU.

Common gotchas

  1. The import name is PIL, not pillow. pip install pillow installs from PIL import Image. import pillow raises ModuleNotFoundError. The legacy name is retained for source compatibility with PIL.
  2. pillow and PIL cannot coexist. If the original PIL is somehow still installed, uninstall it first: pip uninstall PIL pillow && pip install pillow. Mixed installs produce confusing namespace errors.
  3. HEIF / HEIC photos from iPhones won't open. Pillow does not include libheif. Install pillow-heif and call register_heif_opener() once at startup.
  4. WebP write quality defaults are aggressive. Image.save("out.webp") uses quality=80; for archival-quality images pass quality=95, method=6, lossless=True as appropriate.
  5. Security CVEs. Pillow has a long history of buffer-overflow CVEs in native-codec paths — image parsers are large attack surfaces. Always pin to the latest Pillow rather than a fixed older version, and never run user-uploaded images on an old Pillow in a privileged process.
  6. Orientation EXIF is not auto-applied. A JPEG with Orientation=6 loads as raw pixels; Image.size reports pre-rotation dimensions. Use PIL.ImageOps.exif_transpose(img) to apply orientation.
  7. save() infers format from extension only. img.save("photo.jpeg2000") raises — use img.save("photo.jp2", format="JPEG2000"). Pass format= explicitly when the extension is ambiguous or non-standard.

Real-world recipes

Package-level recipes — patterns where the install matrix, codec choice, or production wrapping matters. For pure API usage (modes, draw, EXIF, filters) see the companion Python article.

Recipe 1 — JPEG batch thumbnailer with deterministic output

python
from pathlib import Path
from PIL import Image, ImageOps

SRC = Path("inbox")
DST = Path("thumbs")
DST.mkdir(exist_ok=True)

for src in SRC.glob("*.jpg"):
    with Image.open(src) as im:
        im = ImageOps.exif_transpose(im)        # apply EXIF rotation
        im.thumbnail((512, 512), Image.LANCZOS)
        dst = DST / src.name
        im.save(dst, "JPEG", quality=85, optimize=True, progressive=True)

Output: every inbox/*.jpg becomes a max-512px progressive JPEG in thumbs/. optimize=True runs a second Huffman pass — a few percent smaller files at ~2x encode cost.

Recipe 2 — WebP conversion pipeline (lossless when small, lossy when big)

python
from PIL import Image
from pathlib import Path

def convert(src: Path) -> Path:
    dst = src.with_suffix(".webp")
    with Image.open(src) as im:
        if src.stat().st_size < 100_000:
            im.save(dst, "WEBP", lossless=True, quality=100, method=6)
        else:
            im.save(dst, "WEBP", quality=85, method=6)
    return dst

Output: thumbnails and icons land as lossless WebP, photos as lossy. method=6 is the slowest/best encoder; method=0 is ~20x faster and ~5% larger.

Recipe 3 — watermarking with alpha-composited PNG

python
from PIL import Image

base = Image.open("photo.jpg").convert("RGBA")
wm   = Image.open("logo.png").convert("RGBA")
wm.thumbnail((base.width // 6, base.height // 6))

x = base.width  - wm.width  - 24
y = base.height - wm.height - 24
out = base.copy()
out.alpha_composite(wm, (x, y))
out.convert("RGB").save("watermarked.jpg", quality=90)

Output: watermarked.jpg with logo in the bottom-right corner. Convert back to RGB before JPEG save — JPEG has no alpha channel.

Recipe 4 — HEIF/HEIC iPhone-photo intake

iPhones since 2017 default to HEIC. Pillow needs pillow-heif to read them; register once at module load:

python
from PIL import Image
import pillow_heif

pillow_heif.register_heif_opener()

with Image.open("IMG_1234.HEIC") as im:
    im.convert("RGB").save("IMG_1234.jpg", quality=92)

Output: iPhone photo lands as a JPEG. pillow-heif wraps libheif; both the C library and its codec dependencies (libde265 / x265) must be available on the platform — wheels exist for Linux/macOS/Windows.

Recipe 5 — color-space conversion with ICC profiles

python
from PIL import Image, ImageCms

src    = Image.open("print.tif")
srgb   = ImageCms.createProfile("sRGB")
cmyk   = ImageCms.ImageCmsProfile("USWebCoatedSWOP.icc")  # bundled with print drivers

transform = ImageCms.buildTransform(srgb, cmyk, "RGB", "CMYK")
out = ImageCms.applyTransform(src, transform)
out.save("print_cmyk.tif", compression="tiff_lzw")

Output: TIFF in CMYK color space — the only color model most print shops accept.

Performance tuning

Pillow's hot paths are C — but Python overhead and codec choice still dominate at scale.

Resize resampling

Image.Resampling.LANCZOS is highest quality; Image.Resampling.BILINEAR is ~2x faster with imperceptible loss for typical thumbnails. NEAREST is fastest but only acceptable for pixel art.

python
im.resize((512, 512), Image.LANCZOS)   # best quality
im.resize((512, 512), Image.BILINEAR)  # good default for batch

Output: the same dimensions; LANCZOS keeps high-frequency detail (text edges), BILINEAR is smoother.

thumbnail() vs resize()

thumbnail() modifies in place, respects aspect ratio, and short-circuits when the image is already smaller — preferred for "make this fit in a 512px box".

pillow-simd on Intel/AMD x86_64

pip install pillow-simd is a drop-in fork with SSE4/AVX2-accelerated resize and blur — 4-6x faster for resize, ~2x for blur. Caveat: it shadows pillow (same import name), so pin one or the other in requirements.txt and never both. Not available for ARM (M-series Macs).

Lazy decode

Image.open() does not decode pixels — it reads the header. The full decode happens on the first pixel access (im.load(), iteration, numpy.asarray(im)). For metadata-only inspection, never call .load():

python
with Image.open("huge.tif") as im:
    print(im.size, im.format, im.mode)   # cheap; no decode

Output: instant — even for a 2 GB multi-page TIFF.

In-memory pipelines with BytesIO

For HTTP upload pipelines where the input is bytes and the output is bytes, avoid temp files:

python
from io import BytesIO
from PIL import Image

def process(data: bytes) -> bytes:
    with Image.open(BytesIO(data)) as im:
        im.thumbnail((1024, 1024))
        buf = BytesIO()
        im.save(buf, "JPEG", quality=85)
        return buf.getvalue()

Output: zero disk I/O — typical Lambda/Worker pattern.

Security considerations

Pillow's threat surface is large because image parsers are written in C and image files come from untrusted sources. Treat Pillow as a security-sensitive dependency.

CVE history

The Pillow CVE list runs to dozens of entries — most are buffer overflows or integer-overflow-leading-to-heap-corruption in format decoders (TIFF, JPEG2000, BMP, GIF, EPS, WebP via libwebp). High-impact recent classes:

  • libwebp CVE-2023-4863 — heap overflow in WebP decode; transitively affected every Pillow build that linked the vulnerable libwebp. Pillow shipped a fixed wheel within days, but unpinned older Pillow versions remained vulnerable.
  • libtiff CVEs — recurring; Pillow tracks libtiff upstream but lag is possible.
  • Multi-frame GIF / TIFF DoS — malicious files claiming millions of frames exhaust memory.

Hardening checklist

  • Always run the latest Pillow. Pin only to recent versions (e.g. pillow>=11.0,<12.0), not to an old version. Subscribe to Pillow security advisories.
  • Image.MAX_IMAGE_PIXELS — set to a sane ceiling (e.g. 100_000_000 = ~10000×10000) to reject decompression-bomb inputs. Default is ~89 megapixels and raises DecompressionBombWarning; set Image.MAX_IMAGE_PIXELS = 100_000_000 and treat the warning as an error.
  • Use verify() before decode if you need cheap header validation: Image.open(path).verify() raises on malformed headers without doing a full decode.
  • Run user-image processing in a sandboxed worker — a separate process with RLIMIT_AS, no filesystem access outside /tmp, and a strict timeout. A crashed decoder in the worker doesn't take down the app.
  • Strip EXIF on upload. EXIF can contain ICC profiles, GPS coordinates, and (historically) script payloads. ImageOps.exif_transpose(im) then re-save without metadata.
  • Avoid Image.open(BytesIO(user_bytes)).convert("RGB").save(target_path) in a privileged process. The decode happens between open and save; that's where the buffer overflow lives.

Format-specific risk ordering (anecdotal — verify per advisory)

FormatRiskWhy
JPEGLowlibjpeg-turbo is mature; few recent CVEs
PNGLowzlib is mature
WebPMediumlibwebp had a major 2023 CVE; faster cadence
TIFFHighhuge format spec; libtiff CVEs are recurring
JPEG2000HighOpenJPEG has had multiple CVEs
BMP, GIF, EPSMediumolder format parsers, less actively fuzzed
HEIF (via plugin)Mediumlibheif + libde265 add C surface area

Testing & CI integration

Image-comparison tests need a tolerance — bit-exact comparison fails across libjpeg/libwebp versions and across CPU rounding.

python
from PIL import Image, ImageChops

def images_match(a: Image.Image, b: Image.Image, threshold: int = 8) -> bool:
    if a.size != b.size or a.mode != b.mode:
        return False
    diff = ImageChops.difference(a, b)
    bbox = diff.getbbox()
    if bbox is None:
        return True
    # max channel delta in the diff bbox
    max_delta = max(diff.crop(bbox).getextrema(), key=lambda x: x[1])[1]
    return max_delta <= threshold

Output: True if every pixel is within threshold of the reference. Tune threshold per format — JPEG round-trips can shift by 4-8/255 even when "identical".

For perceptual comparison (more robust across encoder versions):

bash
pip install scikit-image

Output: (none — exits 0 on success)

python
from skimage.metrics import structural_similarity as ssim
import numpy as np
score, _ = ssim(np.asarray(a.convert("L")), np.asarray(b.convert("L")), full=True)
assert score > 0.98

Output: SSIM in [0, 1]; > 0.98 is "visually identical" for most photos.

CI pitfalls:

  • libjpeg version differences cause 1-2/255 pixel deltas. Use tolerance.
  • optimize=True produces deterministic output; without it, JPEG encoder may pad differently across runs.
  • Pillow wheels on aarch64 vs x86_64 occasionally differ at the bit level for WebP encode.

Ecosystem integrations

  • NumPynumpy.asarray(im) produces a (H, W, C) uint8 array; Image.fromarray(arr) is the inverse. Zero-copy in both directions.
  • OpenCVcv2 uses BGR ordering; conversion: cv2.cvtColor(np.asarray(im), cv2.COLOR_RGB2BGR).
  • matplotlibplt.imshow(im) accepts a PIL Image directly; fig.savefig("out.jpg") falls through to Pillow for JPEG/WebP encode.
  • pandas — no built-in bridge, but a Series of PIL Images renders inline in Jupyter via _repr_png_().
  • pillow-heif — HEIC/HEIF support; register_heif_opener() integrates with Image.open.
  • pillow-avif-plugin — AVIF read/write via libavif.
  • pillow-jxl-plugin — JPEG XL support (community).
  • piexif — full-featured EXIF read/write; more complete than Pillow's built-in _getexif.
  • reportlab — embed PIL images directly into PDFs.
  • weasyprint, xhtml2pdf — both use Pillow for image embedding.
  • fastai, torchvision.transforms — both accept PIL images natively and expect them as the canonical input format.

Compatibility matrix

PillowPython floorNotable
11.x3.9Recent stable; libwebp CVE fixes; modern HEIF plugin support
10.x3.8dropped Python 3.7; many resampling-constant renames
9.x3.7mid-line; first to bundle modern libavif/libheif paths
8.x3.6older; security back-patches no longer issued

Format support depends on the wheel build:

FormatWheels (linux x86_64)Wheels (macOS arm64)Wheels (Windows)Notes
JPEGyesyesyeslibjpeg-turbo bundled
PNGyesyesyeszlib bundled
TIFFyesyesyeslibtiff bundled
WebPyesyesyeslibwebp bundled
GIFyesyesyesbuilt-in
BMP, ICOyesyesyesbuilt-in
JPEG2000yesyesyeslibopenjp2; older ARM builds occasionally lacked it
HEIF/HEICno — pluginno — pluginno — pluginneeds pillow-heif
AVIFpartialpartialpartialfull via pillow-avif-plugin
JPEG XLno — pluginno — pluginno — pluginneeds community pillow-jxl-plugin
RAW (.CR2 etc.)nononouse rawpy

When NOT to use this

  • GPU-accelerated transforms at scaletorchvision.transforms.v2 on a GPU, or cv2.cuda, will run circles around Pillow for batch-processing thousands of images per second.
  • Computer-vision algorithms — feature detection, contour finding, optical flow are OpenCV's domain. Pillow is I/O + simple transforms only.
  • Video — Pillow handles animated GIFs and multi-page TIFFs; for MP4/MOV/WebM, use imageio-ffmpeg, pyav, or opencv-python.
  • Scientific image processing — segmentation, morphology, registration: scikit-image and SimpleITK are purpose-built.
  • Streaming gigantic TIFFs (slide microscopy) — tile-based reads need tifffile, pyvips, or openslide. Pillow loads whole frames into memory.
  • Raw camera files (.CR2, .NEF, .ARW) — install rawpy (LibRaw binding); Pillow's raw support is limited.

See also