Skip to content

Pillow library

Official documentation

Pillow is an open-source project maintained by volunteer contributors.

Pillow (imported as PIL) is Python's standard library for opening, editing, and saving image files — photos, screenshots, thumbnails, anything in a common format like JPEG or PNG. It's a third-party package, not part of the standard library, but it's the de facto standard for image work in Python. Like Tkinter, Pillow ultimately produces visual output — a saved or displayed image — which can't be shown inside this site's browser sandbox, so the examples below aren't runnable here. Copy them into a local .py file alongside an image and run them with python to see the results.

Install

pip install pillow

Import

Pillow's package name (pillow) doesn't match its import name — it's imported as PIL, and Image specifically is used throughout this page.

from PIL import Image

Why Pillow?

Pillow is the direct successor to PIL (the original Python Imaging Library, now unmaintained), and has become the standard way to work with images in Python — resizing thumbnails, converting formats, watermarking, or feeding images into a machine learning pipeline. It wraps all of this in one consistent Image object, so once you know how to open, transform, and save an image, the same handful of methods carry over to almost any task.

Python image libraries

  • Pillow — the standard choice for everyday image tasks: opening, resizing, cropping, format conversion, basic drawing.
  • OpenCV — built for computer vision (face detection, video, real-time processing), but a much larger, more complex API for tasks that don't need it.
  • scikit-image — geared toward scientific image analysis (measuring, segmenting), with a more research-oriented API than Pillow's straightforward editing tools.
  • ImageIO — handles reading/writing a wide range of formats, including scientific and video ones, but doesn't offer Pillow's editing operations on top.

For everyday image manipulation, Pillow offers the best balance of simplicity and capability — most tasks are one or two method calls on an Image object.

Concept What it is
Image object A grid of pixel values, plus a bit of metadata (size, mode, format) — nothing more. It has no built-in ability to draw shapes, apply filters, or combine with another image.
Companion module A separate module (ImageDraw, ImageFilter, ImageOps, ...) that operates on an Image from the outside, rather than Image itself growing a method for everything.
Mode How pixel color is stored — "RGB" (red/green/blue), "RGBA" (adds transparency), "L" (grayscale), and others.
Band One channel of a mode — an "RGB" image has three bands (red, green, blue) stacked together.
Size The image's (width, height) in pixels, accessed via .size.
Bounding box A (left, upper, right, lower) tuple of pixel coordinates describing a rectangular region of an image.
Drawing context The object returned by ImageDraw.Draw(img) — every shape/line/text method is called on this, not on the Image itself.
Alpha The transparency value of a pixel, from 0 (fully invisible) to 255 (fully solid) — only present on images in "RGBA" mode.
Frame One image within an animated file (like a GIF) — ImageSequence is what lets you loop over all of them.

Beyond the base Image object, Pillow's functionality is spread across several companion modules under the PIL package:

Module Used for
ImageDraw Drawing shapes, lines, and polygons directly onto an image.
ImageFont Loading a custom font, for then drawing text with ImageDraw.
ImageColor Converting a color name or hex string into an (r, g, b) tuple.
ImageFilter Applying ready-made pixel effects — blur, sharpen, edge-detection.
ImageEnhance Dialing brightness, contrast, color, or sharpness up or down by an exact amount.
ImageOps One-line convenience operations — auto-contrast, mirroring, inverting.
ImageChops Combining two same-size images pixel by pixel — diffing, blending.
ImageSequence Looping over every frame of an animated image, like a GIF.

The Image

The Image object is where every Pillow workflow starts and ends — opening a file, transforming it, and saving the result all happen through methods on this one class.

Opening and saving images

Every Pillow workflow starts the same way: open a file into an Image object, do something to it, then save the result — Pillow infers the file format from the extension you save to, so converting formats is often just a matter of changing the file extension.

from PIL import Image

img = Image.open("snake.jpg")
print(img.size, img.mode)   # (800, 600) RGB

img.save("snake_copy.png")
Opening a file

Reads a file's header immediately but delays loading the full pixel data until you need it. Fast even for large images if all you want is its size or format. .format reports the file type Pillow detected, .size gives (width, height), and .mode gives its color mode.

img = Image.open("snake.jpg")
print(img.format, img.size, img.mode)   # JPEG (800, 600) RGB
from PIL import Image

img = Image.open("snake.jpg")
print(img.format, img.size, img.mode)
Saving a file

Writes the image to disk, picking the file format from the extension — unless you pass format= explicitly. Some formats accept extra keyword options — JPEG's quality (0–100) trades file size for image quality, for example.

img.save("snake_copy.png")            # format inferred from ".png"
img.save("snake_copy.jpg", quality=85)  # JPEG-specific option
from PIL import Image

img = Image.open("snake.jpg")
img.save("snake_copy.png")
img.save("snake_copy.jpg", quality=85)
Displaying an image

Opens the image in whatever program your operating system uses to view images. Handy for a quick look while writing a script, since it doesn't require saving a file first. It's meant for local development rather than production code, since it depends on external programs actually being installed.

img.show()   # opens in your OS's default image viewer
from PIL import Image

img = Image.open("snake.jpg")
img.show()

Basic operations

Pillow's core editing operations — resizing, cropping, rotating, flipping — are all methods on an Image that return a new Image, leaving the original untouched.

resized = img.resize((400, 300))
cropped = img.crop((0, 0, 200, 200))
rotated = img.rotate(90)

Resize

Scales the image to an exact new size. .resize((width, height)) doesn't preserve the original aspect ratio for you, so stretching happens if the new dimensions don't match the original proportions. For a quick, ratio-preserving thumbnail instead, use .thumbnail((max_width, max_height)), which resizes in place rather than returning a new image.

thumbnail = img.resize((200, 150))
print(thumbnail.size)   # (200, 150)
from PIL import Image

img = Image.open("snake.jpg")
thumbnail = img.resize((200, 150))
print(thumbnail.size)

Crop

Takes a bounding box and returns just that rectangular region. .crop() takes (left, upper, right, lower) pixel coordinates. (0, 0) is the top-left corner of the image, with x increasing rightward and y increasing downward.

cropped = img.crop((50, 50, 250, 200))   # left, upper, right, lower
print(cropped.size)   # (200, 150)
from PIL import Image

img = Image.open("snake.jpg")
cropped = img.crop((50, 50, 250, 200))
print(cropped.size)

Rotate and flip

.rotate(degrees) rotates counter-clockwise around the image's center. Pass expand=True to grow the canvas so corners aren't clipped off (without it, the image keeps its original size and rotated corners are cropped away). .transpose() handles flips and 90°-multiple rotations without any clipping concerns, using constants like Image.FLIP_LEFT_RIGHT or Image.ROTATE_90.

rotated = img.rotate(90, expand=True)
flipped = img.transpose(Image.FLIP_LEFT_RIGHT)
from PIL import Image

img = Image.open("snake.jpg")
rotated = img.rotate(90, expand=True)
flipped = img.transpose(Image.FLIP_LEFT_RIGHT)
print(rotated.size, flipped.size)

Image modes

An image's mode determines how each pixel's color is stored — how many bands it has and what each one means. Converting between modes is a single method call, and it's often a required first step before an operation that only works on one mode (like grayscale-only filters). .convert(mode) returns a new image re-encoded into the given mode — "L" collapses color down to a single grayscale band; "RGBA" adds an alpha (transparency) band on top of red/green/blue, where 0 is fully transparent and 255 is fully opaque.

grayscale = img.convert("L")     # single band, 0 (black) to 255 (white)
rgba = img.convert("RGBA")       # adds a 4th, transparency band
print(grayscale.mode, rgba.mode)  # L RGBA
from PIL import Image

img = Image.open("snake.jpg")
grayscale = img.convert("L")
rgba = img.convert("RGBA")
print(grayscale.mode, rgba.mode)

ImageOps module

The ImageOps module collects common one-line transforms that would otherwise take several steps to write by hand — contrast fixes, mirroring, and color inversion among them.

from PIL import ImageOps

fixed = ImageOps.autocontrast(img)
mirrored = ImageOps.mirror(img)

Common ImageOps functions

.autocontrast() stretches an image's darkest and lightest pixels out to pure black and white, which can fix a flat, washed-out photo without manually tuning ImageEnhance.Contrast. .mirror()/.flip() cover the same ground as .transpose() with more direct names. .invert() flips every pixel to its opposite color — it only works on "RGB" (or "L") images, so convert first if the source has an alpha band.

fixed = ImageOps.autocontrast(img)   # stretches contrast to use the full range
mirrored = ImageOps.mirror(img)      # flips left-to-right
inverted = ImageOps.invert(img.convert("RGB"))   # like a photo negative
from PIL import Image, ImageOps

img = Image.open("snake.jpg")
fixed = ImageOps.autocontrast(img)
mirrored = ImageOps.mirror(img)
inverted = ImageOps.invert(img.convert("RGB"))
fixed.save("fixed.jpg")

ImageDraw module

An Image object is really just a grid of pixel values — it has no drawing tools of its own. ImageDraw is the first example of a companion module: a separate class that wraps an Image and adds one specific ability, here turning it into a canvas you can draw directly onto — shapes and lines, useful for annotating a photo or generating an image from scratch rather than editing an existing file. ImageFont, ImageFilter, ImageEnhance, ImageOps, and the modules further down this page all follow the same pattern: they act on an Image from the outside, rather than Image itself growing a method for everything.

from PIL import ImageDraw

draw = ImageDraw.Draw(img)
draw.rectangle((10, 10, 100, 60), outline="green", width=3)
draw.text((15, 20), "ball python", fill="green")

Shapes and lines

ImageDraw.Draw(img) creates a drawing context bound to an image. Every call on it modifies img directly, in place. .rectangle(), .ellipse(), and .line() each take a bounding box or set of coordinates, plus outline/fill colors and an optional width.

draw.rectangle((10, 10, 100, 60), outline="green", width=3)
draw.ellipse((20, 20, 80, 50), fill="yellow")
draw.line((0, 0, 100, 100), fill="black", width=2)
from PIL import Image, ImageDraw

img = Image.new("RGB", (120, 80), "white")
draw = ImageDraw.Draw(img)
draw.rectangle((10, 10, 100, 60), outline="green", width=3)
draw.ellipse((20, 20, 80, 50), fill="yellow")
img.save("shapes.png")
Polygons

Draws any straight-edged shape from a list of (x, y) points. .polygon() connects the points in order, with the last point automatically connected back to the first. Unlike .rectangle()/.ellipse(), there's no bounding-box shortcut: you calculate each corner's coordinates yourself, usually from a center position and size.

points = [(60, 10), (110, 70), (10, 70)]   # a triangle
draw.polygon(points, fill="green")
from PIL import Image, ImageDraw

img = Image.new("RGB", (120, 80), "white")
draw = ImageDraw.Draw(img)
points = [(60, 10), (110, 70), (10, 70)]
draw.polygon(points, fill="green")
img.save("triangle.png")
Transparent colors

A color can include a fourth number — alpha — to draw something translucent, from 0 (fully invisible) to 255 (fully solid), so shapes underneath still show through. This only works on an image in "RGBA" mode; drawing an RGBA color onto a plain "RGB" image just silently drops the transparency.

draw.ellipse((20, 20, 80, 80), fill=(255, 0, 0, 120))   # translucent red
from PIL import Image, ImageDraw

img = Image.new("RGBA", (120, 80), "white")
draw = ImageDraw.Draw(img)
draw.ellipse((10, 10, 70, 70), fill=(255, 0, 0, 255))
draw.ellipse((40, 30, 100, 70), fill=(0, 0, 255, 120))   # overlaps, translucent
img.save("overlap.png")
Drawing with objects

Once a drawing gets complicated, it's common to wrap each thing you're drawing in its own class — an object that stores its own position/size/color, and knows how to draw itself given a drawing context. Nothing here is Pillow-specific: it's the same pattern covered in Classes — bundling data with the behavior that acts on it — just applied to a shape instead of a snake. A calling function loops over a list of these objects and calls .draw() on each, so building a complex image — dozens of randomly placed shapes, say, using the random module — is just a loop appending new Shape objects rather than dozens of manual draw_context calls.

class Shape:
    def __init__(self, position, size, color):
        self.position = position
        self.size = size
        self.color = color

    def draw(self, draw_context):
        x, y = self.position
        half = self.size / 2
        box = (x - half, y - half, x + half, y + half)
        draw_context.ellipse(box, fill=self.color)
import random
from PIL import Image, ImageDraw


class Shape:
    def __init__(self, position, size, color):
        self.position = position
        self.size = size
        self.color = color

    def draw(self, draw_context):
        x, y = self.position
        half = self.size / 2
        box = (x - half, y - half, x + half, y + half)
        draw_context.ellipse(box, fill=self.color)

img = Image.new("RGB", (200, 200), "white")
draw_context = ImageDraw.Draw(img)
palette = ["green", "yellow", "brown"]

shapes = []
for _ in range(10):
    position = (random.randint(0, 200), random.randint(0, 200))
    size = random.randint(10, 40)
    color = random.choice(palette)
    shapes.append(Shape(position, size, color))

for shape in shapes:
    shape.draw(draw_context)

img.save("generated.png")

ImageFont module

ImageDraw.text() works with no extra setup, but falls back to a small built-in bitmap font. ImageFont loads an actual .ttf font file at a chosen size, for anything larger or more legible.

from PIL import ImageFont

font = ImageFont.truetype("arial.ttf", 20)
draw.text((10, 10), "burmese python", fill="black", font=font)

Loading a font

Loads a .ttf (or .otf) font file at a specific point size. ImageFont.truetype(path, size) returns a font object to pass into draw.text(..., font=font). The path can be a font file sitting next to your script, or a system font's full path — sizes aren't interchangeable between fonts, so reload at a new size rather than trying to scale a loaded font after the fact.

font = ImageFont.truetype("arial.ttf", 20)
draw.text((10, 10), "burmese python", fill="black", font=font)
from PIL import Image, ImageDraw, ImageFont

img = Image.new("RGB", (200, 60), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("arial.ttf", 20)
draw.text((10, 15), "burmese python", fill="black", font=font)
img.save("labeled.png")

ImageColor module

Drawing methods accept a color as a plain name ("green") or a hex string ("#3f6b52"), but sometimes you need that same color as an actual (r, g, b) tuple — to do math on it, blend it with another color, or store it in a data structure like the Shape class above. ImageColor.getrgb() converts either format into the tuple Pillow uses internally.

from PIL import ImageColor

rgb = ImageColor.getrgb("green")        # (0, 128, 0)
rgb2 = ImageColor.getrgb("#3f6b52")     # (63, 107, 82)

Converting color names

Accepts most CSS-style color names and #rrggbb/#rgb hex strings, returning a plain (r, g, b) tuple. .getrgb() returns (r, g, b, a) if the input included transparency. Useful once a palette is defined as hex codes rather than named colors, or when a color needs to be manipulated as numbers rather than passed straight into a drawing method.

green_rgb = ImageColor.getrgb("green")
hex_rgb = ImageColor.getrgb("#3f6b52")
print(green_rgb, hex_rgb)
from PIL import ImageColor

green_rgb = ImageColor.getrgb("green")
hex_rgb = ImageColor.getrgb("#3f6b52")
print(green_rgb, hex_rgb)

ImageFilter module

Beyond geometric edits, ImageFilter can adjust an image's look — blurring, sharpening, or tracing its edges — by applying a ready-made pixel transformation, no convolution or kernel math required.

from PIL import ImageFilter

blurred = img.filter(ImageFilter.BLUR)

Applying a filter

Applies one of Pillow's built-in filter presets, each a ready-made pixel transformation. .filter()ImageFilter.CONTOUR traces edges into a sketch-like outline, distinct from FIND_EDGES, which highlights edges while keeping the rest of the image dark.

blurred = img.filter(ImageFilter.BLUR)
sharpened = img.filter(ImageFilter.SHARPEN)
edges = img.filter(ImageFilter.FIND_EDGES)
outlined = img.filter(ImageFilter.CONTOUR)
from PIL import Image, ImageFilter

img = Image.open("snake.jpg")
blurred = img.filter(ImageFilter.BLUR)
sharpened = img.filter(ImageFilter.SHARPEN)
outlined = img.filter(ImageFilter.CONTOUR)
blurred.save("blurred.jpg")
sharpened.save("sharpened.jpg")
outlined.save("outlined.jpg")

ImageEnhance module

Where ImageFilter applies a fixed preset, ImageEnhance lets you dial an existing quality — brightness, contrast, color, sharpness — up or down by an exact amount.

from PIL import ImageEnhance

brighter = ImageEnhance.Brightness(img).enhance(1.5)

Enhancing an image

Each ImageEnhance class wraps an image and exposes .enhance(factor). Brightness, Contrast, Color, Sharpness1.0 leaves the image unchanged, below 1.0 reduces the effect, and above 1.0 increases it. Color controls saturation specifically: pushed toward 0.0 the image slides to grayscale, pushed well above 1.0 colors become more vivid and saturated.

brighter = ImageEnhance.Brightness(img).enhance(1.5)   # 1.0 = unchanged
higher_contrast = ImageEnhance.Contrast(img).enhance(1.3)
more_colorful = ImageEnhance.Color(img).enhance(2.0)   # boost saturation
from PIL import Image, ImageEnhance

img = Image.open("snake.jpg")
brighter = ImageEnhance.Brightness(img).enhance(1.5)
higher_contrast = ImageEnhance.Contrast(img).enhance(1.3)
more_colorful = ImageEnhance.Color(img).enhance(2.0)
brighter.save("brighter.jpg")
more_colorful.save("more_colorful.jpg")

ImageChops module

Everything so far transforms a single image. ImageChops ("channel operations") instead combines two images of the same size, pixel by pixel — spotting what changed between two photos, or blending one image into another.

from PIL import ImageChops

diff = ImageChops.difference(before, after)
blended = ImageChops.multiply(img, mask)

Comparing and combining images

.difference(im1, im2) subtracts one image from the other pixel by pixel. Identical areas come out solid black, and anything that changed shows up as a bright patch. Calling .getbbox() on the result gives the bounding box of everything that differs (or None if the two images are pixel-for-pixel identical), a quick way to check "did anything change?" without comparing every pixel yourself. .multiply()/.screen()/.add() combine two images with different blending math, similar to layer blend modes in photo-editing software.

diff = ImageChops.difference(before, after)
diff.getbbox()   # bounding box of everything that changed, or None if identical
from PIL import Image, ImageChops

before = Image.open("snake_before.jpg")
after = Image.open("snake_after.jpg")

diff = ImageChops.difference(before, after)
print(diff.getbbox())
diff.save("diff.jpg")

Format conversion

Because .save() infers the output format from the file extension, converting between formats is usually just an open-then-save with a different name — with a couple of format-specific details worth knowing.

img = Image.open("snake.png")
img.convert("RGB").save("snake.jpg")   # JPEG has no transparency, so drop RGBA first

Converting between formats

JPEG doesn't support transparency, so saving an "RGBA" image straight to .jpg raises an error. Convert to "RGB" first, which drops the alpha band. PNG, by contrast, supports both "RGB" and "RGBA" natively, so no conversion is needed going the other direction.

img = Image.open("snake.png")        # RGBA, with transparency
img.convert("RGB").save("snake.jpg")  # JPEG can't store alpha — convert first
from PIL import Image

img = Image.open("snake.png")
img.convert("RGB").save("snake.jpg")

ImageSequence module

An animated GIF is really a whole stack of images shown one after another. Image.open() only gives you the first frame by default — ImageSequence lets a for loop step through every frame in order.

from PIL import Image, ImageSequence

gif = Image.open("snake_slither.gif")
for frame in ImageSequence.Iterator(gif):
    frame.save(f"frame_{frame.tell()}.png")

Looping over GIF frames

Hands a for loop one frame at a time, in order, from an animated image. ImageSequence.Iterator(img) — each frame is a regular Image object, so every operation covered on this page (resize, filter, draw) works on it the same way. .tell() reports which frame number you're currently on, useful for numbering saved output files.

for frame in ImageSequence.Iterator(gif):
    print(frame.tell(), frame.size)   # frame index, then its size
from PIL import Image, ImageSequence

gif = Image.open("snake_slither.gif")
for frame in ImageSequence.Iterator(gif):
    print(frame.tell(), frame.size)
    frame.save(f"frame_{frame.tell()}.png")

Putting it together

Pillow doesn't need anything special to combine with the rest of Python — a function wrapping one transformation, called from an if/elif chosen by user input, looped until the user's done, is enough to build a small interactive tool out of the operations above.

def apply_filter(img, choice):
    if choice == "blur":
        return img.filter(ImageFilter.BLUR)
    elif choice == "grayscale":
        return img.convert("L")
    elif choice == "sharpen":
        return img.filter(ImageFilter.SHARPEN)
    else:
        return img

An interactive filter tool

Combines a function, an if/elif chain, and a while loop — nothing here is Pillow-specific. Each piece here is something covered elsewhere on this site — a function wrapping one transformation, an if/elif chain picking which one to run, and a while loop that keeps asking until the user's satisfied. Pillow itself only shows up inside apply_filter.

while True:
    choice = input("filter (blur/grayscale/sharpen/done): ")
    if choice == "done":
        break
    result = apply_filter(img, choice)
    result.save(f"{choice}.jpg")
    print(f"saved {choice}.jpg")
from PIL import Image, ImageFilter


def apply_filter(img, choice):
    if choice == "blur":
        return img.filter(ImageFilter.BLUR)
    elif choice == "grayscale":
        return img.convert("L")
    elif choice == "sharpen":
        return img.filter(ImageFilter.SHARPEN)
    else:
        return img

img = Image.open("snake.jpg")

while True:
    choice = input("filter (blur/grayscale/sharpen/done): ")
    if choice == "done":
        break
    result = apply_filter(img, choice)
    result.save(f"{choice}.jpg")
    print(f"saved {choice}.jpg")