pillow
Pillow is the maintained fork of PIL, the original Python Imaging Library, and it is the default way Python code opens, edits, and saves images. It reads and writes a long list of formats (JPEG, PNG, WebP, GIF, TIFF, BMP and more) and covers everyday operations: resize, crop, rotate, convert color modes, draw text and shapes, apply filters, and read EXIF metadata. Nearly every Python web framework, ML pipeline, and thumbnail job touches Pillow somewhere, often as a transitive dependency.
The safe default for image manipulation in Python and one of the healthiest projects on PyPI: 131M weekly downloads, pushed within the last day, low open-issue count. Reach past it only when you need vision algorithms (OpenCV) or serious throughput (libvips).
Use it if
- You need everyday image work in Python: thumbnails, format conversion, crops, watermarks, or reading EXIF data
- You want the ecosystem default; Django ImageField, torchvision transforms, and most upload-handling tutorials all assume Pillow
- You need broad format support in one dependency, including animated GIF and WebP read/write
- You process untrusted uploads and want a library whose security posture is active (OSS-Fuzz fuzzing, private vulnerability reporting, roughly quarterly releases)
- Your workload is computer vision (feature detection, video, contours); opencv-python or scikit-image are built for analysis, Pillow is built for manipulation
- You are doing high-throughput server-side resizing; Pillow is CPU-bound pure processing and libraries built on libvips (for example pyvips) are several times faster with lower memory use
- You only ever touch images as numpy arrays in a scientific pipeline; imageio or scikit-image keep you in array land without Pillow's Image object in the middle
- You need exotic scientific formats (DICOM, specialized microscopy) as first-class citizens; Pillow's support there is thin or absent
Setup reality
pip install pillow just works on all common platforms because the project ships prebuilt wheels with libjpeg, zlib, and friends bundled; Python 3.10+ is required for the 12.x line. The pain shows up in edge cases: on Alpine or unusual architectures pip may compile from source and then you need C headers for every codec you want; the package installs as 'pillow' but imports as 'PIL', which confuses every newcomer; and installing pillow alongside a leftover PIL or pillow-simd in one environment causes import shadowing. Some features (WebP animation, certain TIFF compressions) silently depend on how the wheel was built.
Patterns
Open, resize, and save an imageopen-resize-save
from PIL import Image
with Image.open("photo.jpg") as im:
resized = im.resize((800, 600))
resized.save("photo_800.jpg", quality=85)resize ignores aspect ratio and returns a new image; use thumbnail if you want the ratio preserved.
Make a thumbnail that keeps aspect ratiomake-thumbnail
from PIL import Image
with Image.open("photo.jpg") as im:
im.thumbnail((400, 400)) # modifies im in place
im.save("thumb.jpg")thumbnail mutates the image in place, returns None, and only ever shrinks; it will not upscale small images.
Convert RGBA to RGB before saving JPEGconvert-for-jpeg
from PIL import Image
with Image.open("logo.png") as im:
if im.mode in ("RGBA", "P"):
im = im.convert("RGB")
im.save("logo.jpg", quality=90)JPEG has no alpha channel; saving an RGBA or palette image as JPEG raises OSError unless you convert first.
Crop a regioncrop-image
from PIL import Image
with Image.open("photo.jpg") as im:
box = (100, 100, 500, 400) # left, upper, right, lower
region = im.crop(box)
region.save("crop.jpg")The box is (left, upper, right, lower) with (0,0) at the top-left; crop is lazy, so call load() or save() before closing the source file.
Auto-rotate phone photos using EXIFfix-exif-orientation
from PIL import Image, ImageOps
with Image.open("phone_photo.jpg") as im:
im = ImageOps.exif_transpose(im)
im.save("upright.jpg")Phone cameras store rotation in EXIF instead of rotating pixels; skip this and portrait uploads render sideways.
Draw text on an imagedraw-text
from PIL import Image, ImageDraw, ImageFont
with Image.open("photo.jpg") as im:
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("DejaVuSans.ttf", 48)
draw.text((20, 20), "Hello", font=font, fill="white",
stroke_width=2, stroke_fill="black")
im.save("labeled.jpg")The default bitmap font is tiny and cannot scale; pass a real .ttf path via ImageFont.truetype, and remember font files are not bundled with Pillow.
Apply a Gaussian blur or other filterapply-blur
from PIL import Image, ImageFilter
with Image.open("photo.jpg") as im:
blurred = im.filter(ImageFilter.GaussianBlur(radius=6))
sharp = im.filter(ImageFilter.SHARPEN)
blurred.save("blurred.jpg")Filters return new images; GaussianBlur radius scales with image size, so a fixed radius looks different on thumbnails vs originals.
Overlay a transparent watermarkpaste-watermark
from PIL import Image
with Image.open("photo.jpg") as base, Image.open("mark.png") as mark:
base = base.convert("RGBA")
x = base.width - mark.width - 16
y = base.height - mark.height - 16
base.paste(mark, (x, y), mark) # third arg = alpha mask
base.convert("RGB").save("watermarked.jpg")Pass the watermark itself as the third paste argument to use its alpha channel; omit it and transparent pixels paste as solid black.
Convert between Pillow images and numpy arraysnumpy-interop
import numpy as np
from PIL import Image
with Image.open("photo.jpg") as im:
arr = np.asarray(im) # HxWx3 uint8
arr = (arr * 0.5).astype("uint8")
out = Image.fromarray(arr)
out.save("darker.jpg")np.asarray gives a read-only view for some modes; use np.array(im) when you need a writable copy, and keep dtype uint8 for fromarray.
Open an upload from bytes and re-encodeimage-from-bytes
import io
from PIL import Image
data: bytes = request_file_bytes
im = Image.open(io.BytesIO(data))
im.verify() # cheap integrity check; image must be reopened after
im = Image.open(io.BytesIO(data))
buf = io.BytesIO()
im.save(buf, format="WEBP", quality=80)
webp_bytes = buf.getvalue()verify() invalidates the image object, so reopen before using it; very large uploads can also trip Pillow's decompression bomb warning (Image.MAX_IMAGE_PIXELS).
Read EXIF metadataread-exif-metadata
from PIL import Image
from PIL.ExifTags import TAGS
with Image.open("photo.jpg") as im:
exif = im.getexif()
for tag_id, value in exif.items():
print(TAGS.get(tag_id, tag_id), value)getexif returns numeric tag IDs; map them through PIL.ExifTags.TAGS, and expect many images (screenshots, PNGs) to have no EXIF at all.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| opencv-python | PyPI | You need computer vision (detection, video, transforms) rather than image file editing |
| scikit-image | PyPI | You want algorithmic image processing on numpy arrays in a scientific stack |
| imageio | PyPI | You just need to read/write images and video frames as arrays with minimal API |