Documentation Menu

Pillow Handbook

Independent practical guide. Canonical source: official Pillow tutorial.

This handbook walks through the everyday Pillow workflow in the order you usually need it: open a file and inspect it, save it in another format, cut out and paste regions, resize and rotate, change colours, and enhance the result. Each section is a short, practical summary with code you can adapt.

Two ideas make the rest easier. First, an Image object is just pixel data plus a mode (how each pixel is stored, such as "RGB" or "L") and a size. Second, almost every operation returns a new image and leaves the original untouched; the main exceptions are thumbnail(), paste() and drawing with ImageDraw, which change the image in place.

PIL vs Pillow: Pillow is a friendly fork of the Python Imaging Library (PIL). PIL's last release was 1.1.7 in 2009 and it never supported Python 3; Pillow has carried the project forward since 2010 with Python 3 support, new formats and security fixes. Always use from PIL import Image — not import Pillow.

Using the Image Class

The most important class in Pillow is the Image class. You can create instances by loading from files, processing other images, or creating from scratch.

Opening an Image
from PIL import Image im = Image.open("photo.jpg") print(im.format, im.size, im.mode) # JPEG (1600, 1067) RGB (format, (width, height), mode) # Display the image (opens in default viewer) im.show()
AttributeTypeDescription
im.formatstr | NoneSource file format (e.g. 'JPEG', 'PNG'). None for programmatically created images.
im.modestrPixel format: '1', 'L', 'P', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F'
im.sizetupleImage dimensions as (width, height) in pixels.
im.infodictMetadata dictionary populated by the file decoder (DPI, copyright, comments, EXIF).
im.widthintImage width in pixels (shortcut for im.size[0]).
im.heightintImage height in pixels (shortcut for im.size[1]).

Reading & Writing Files

Pillow can read and write over 30 image formats. The format is auto-detected on read, and inferred from the file extension on write.

Convert & Save
import sys from PIL import Image # Convert any image to PNG with Image.open(sys.argv[1]) as im: im.save(sys.argv[2]) # Convert PNG to JPEG (must drop alpha channel) img = Image.open("icon.png").convert("RGB") img.save("icon.jpg", quality=95) # Create a thumbnail — modifies in place, preserves aspect ratio img = Image.open("photo.jpg") img.thumbnail((128, 128)) img.save("thumb.jpg")

Using StringIO / BytesIO

In-Memory Buffer
from PIL import Image import io # Save to in-memory buffer (useful for web APIs) img = Image.open("photo.jpg") buf = io.BytesIO() img.save(buf, format="WEBP", quality=85) buf.seek(0) # buf.read() returns the raw bytes

Cutting, Pasting & Merging

The Image class contains methods to cut out sub-rectangles, paste them back, and merge separate bands into one image.

Copy / Paste Regions
im = Image.open("hopper.ppm") # Define a box: (left, upper, right, lower) box = (100, 100, 400, 400) region = im.crop(box) # Rotate region 180 degrees region = region.transpose(Image.Transpose.ROTATE_180) # Paste it back im.paste(region, box) im.save("patched.jpg") # Merge: split RGB into bands, swap R and B, merge r, g, b = im.split() merged_im = Image.merge("RGB", (b, g, r))

Geometric Transforms

Pillow supports resizing, rotating, and more complex geometric transforms via the transform() method.

MethodSignatureNotes
resize()resize(size, resample=None, box=None, reducing_gap=None)Returns resized copy. Resample defaults to BICUBIC: NEAREST, BILINEAR, BICUBIC, LANCZOS
thumbnail()thumbnail(size, resample=BICUBIC)Modifies in-place. Preserves aspect ratio. Never upscales.
crop()crop(box)Returns rectangular sub-region. Box: (left, upper, right, lower)
rotate()rotate(angle, expand=False, center=None, translate=None)Counter-clockwise. expand=True grows canvas to fit.
transpose()transpose(method)Flip/rotate. Methods: FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90/180/270, TRANSPOSE, TRANSVERSE
transform()transform(size, method, data, resample)Affine, perspective, mesh, quad transforms.
Transforms Example
out = im.resize((128, 128), resample=Image.Resampling.LANCZOS) out = im.rotate(45, expand=True, fillcolor="white") out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)

Color Transforms

Pillow can convert between image modes using convert(). It supports dithering when converting from colour to palette mode.

Mode Conversion
# Convert to grayscale gray = im.convert("L") # Convert to palette mode (256 colors) with dithering p_img = im.convert("P", dither=Image.Dither.FLOYDSTEINBERG) # Point transform: apply a lookup table lut = [i ** 2 // 255 for i in range(256)] out = im.point(lut) # Apply a different function per-band r, g, b = im.split() r = r.point(lambda i: i * 1.4) # boost red 40% out = Image.merge("RGB", (r, g, b))

Image Enhancement

The ImageEnhance module provides a standardized API for contrast, brightness, color saturation, and sharpness adjustment. Each class takes an Image and returns an enhancer object; call .enhance(factor) where 1.0 = no change.

Enhancement Pipeline
from PIL import Image, ImageEnhance img = Image.open("photo.jpg") # Increase contrast by 50% img = ImageEnhance.Contrast(img).enhance(1.5) # Increase sharpness (0.0=blurred, 1.0=original, 2.0=enhanced) img = ImageEnhance.Sharpness(img).enhance(2.0) # Boost saturation img = ImageEnhance.Color(img).enhance(1.2) # Increase brightness img = ImageEnhance.Brightness(img).enhance(1.1) img.save("enhanced.jpg")

Image Sequences (Animations)

Pillow has basic support for image sequences (animation formats like GIF and animated WebP). When you open a sequence file, Pillow automatically loads the first frame.

GIF Frame Extraction
from PIL import Image, ImageSequence img = Image.open("animation.gif") print(f"Frames: {img.n_frames}, animated: {img.is_animated}") for i, frame in enumerate(ImageSequence.Iterator(img)): frame.save(f"frame_{i:04d}.png") # Seek to a specific frame number img.seek(5) img.save("frame_5.png")

Creating a GIF

Creating Animated GIF
frames = [Image.open(f"frame_{i}.png") for i in range(10)] frames[0].save( "output.gif", save_all=True, append_images=frames[1:], duration=100, # ms per frame loop=0 # 0 = loop forever )

Postscript Printing

Pillow includes a PSDraw module for drawing on Postscript printers. You can set the text font, rotate text, and draw images. This is primarily useful for print pipelines and legacy workflows.

Postscript Output
from PIL import Image, PSDraw img = Image.open("photo.jpg") title = "Launch Pad 39B" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # 1 x 2 inch to 7 x 10 inch, in points with open("print.ps", "wb") as f: # PSDraw writes bytes: open the file in binary mode ps = PSDraw.PSDraw(f) ps.begin_document(title) ps.image(box, img, 75) # 75 dpi ps.setfont("Helvetica-Bold", 36) ps.text((3 * 72, 4 * 72), title) ps.end_document()

Batch Processing

Pillow is used extensively in batch workflows — converting, resizing, or watermarking large collections of images. Here are the key patterns.

Batch Resize All JPEGs
from PIL import Image from pathlib import Path input_dir = Path("./photos") output_dir = Path("./thumbnails") output_dir.mkdir(exist_ok=True) for img_path in input_dir.glob("*.jpg"): with Image.open(img_path) as img: img.thumbnail((800, 800), Image.Resampling.LANCZOS) img.save(output_dir / img_path.name, quality=85) print(f"Processed: {img_path.name}")