mrkeyoor.com_
Wed 05 Aug 05:05 UTC
PyPIUtilsupdated 05 Aug 2026

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.

Verdict

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).

API stability4/5Core Image API has been stable for a decade; majors arrive yearly on a published schedule and do remove long-deprecated constants (like the ANTIALIAS rename to LANCZOS), so pinned old code can break on upgrade.
Docs4/5pillow.readthedocs.io has a full reference plus a tutorial-style handbook; weaker on task-oriented recipes, so common jobs like watermarking get pieced together from Stack Overflow.
Maintenance5/5Pushed August 4, 2026, 151 open issues on a 131M-weekly-download package, quarterly release cadence, OSS-Fuzz coverage, and Tidelift-backed security handling.
Ecosystem5/5It is the substrate: Django, torchvision, matplotlib, and countless upload pipelines depend on it, and nearly every Python imaging answer online assumes PIL.Image.

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)
Skip it if

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

PackageRegistryPick it when
opencv-pythonPyPIYou need computer vision (detection, video, transforms) rather than image file editing
scikit-imagePyPIYou want algorithmic image processing on numpy arrays in a scientific stack
imageioPyPIYou just need to read/write images and video frames as arrays with minimal API