Documentation Menu

Image Module

Independent quick-reference. Canonical source: official Image module documentation.

PIL.Image is the module you use in almost every Pillow script. It contains the Image class (one object per picture) and the functions that create those objects: Image.open() for files, Image.new() for blank canvases, and Image.fromarray() / Image.frombytes() for pixel data you already have in memory.

Import
from PIL import Image

Image.open(), BytesIO & Lazy Loading

Opens an image from a path or a binary file object. Opening is cheap: Pillow reads only the header (format, size, mode) and decodes the pixels later, the first time you use them or call load(). That makes it fast to check the dimensions of thousands of files, but it also means the file stays open until the pixels are loaded or you close it, so prefer with Image.open(path) as img: in loops. If the content is not a format Pillow recognises, it raises UnidentifiedImageError.

ParameterTypeDescription
fpstr | Path | file-likeA filename (string), pathlib.Path object, or an object implementing read(), seek(), tell().
modestrThe mode. If given, this argument must be "r" (the only option).
formatslist | NoneIf given, only these formats will be attempted. Accepts a sequence of format identifiers.
Image.open() Examples
from PIL import Image # Open from file path im = Image.open("photo.jpg") # Open from file object with open("photo.jpg", "rb") as f: im = Image.open(f) im.load() # Must load before closing file # Open from BytesIO buffer import io buf = io.BytesIO(image_bytes) im = Image.open(buf) # Limit to specific formats im = Image.open("file.jpg", formats=["JPEG", "PNG"])

Image.new()

Makes a blank image of a given mode and size, filled with a colour (black by default). Use it for canvases, masks and backgrounds.

ParameterTypeDescription
modestrThe mode to use for the new image (e.g., "RGB", "RGBA", "L").
sizetuple[int,int]Image size as a (width, height) tuple.
colorint | tuple | strFill colour. Default: 0. Can be a named colour like "red", a tuple (255,0,0), or an integer for single-band modes.
Image.new() Examples
# Create a 200x200 solid red RGB image img = Image.new("RGB", (200, 200), "red") # Create a transparent 400x300 RGBA canvas canvas = Image.new("RGBA", (400, 300), (0, 0, 0, 0)) # Create a black 100x100 grayscale image gray = Image.new("L", (100, 100), 0)

Image.frombytes()

Builds an image from raw bytes when you already know the mode and size, for example frames from a camera SDK or a hardware device. The bytes must be exactly width × height × bytes-per-pixel long.

ParameterTypeDescription
modestrImage mode.
sizetuple(width, height).
databytesRaw pixel data.
decoder_namestrDecoder to use. Default: "raw".
*args—Arguments passed to the decoder.
frombytes Example
# Create from raw bytes (RGBA, 2x2 image) data = bytes([255,0,0,255, # red 0,255,0,255, # green 0,0,255,255, # blue 255,255,0,255]) # yellow img = Image.frombytes("RGBA", (2, 2), data)

Image.fromarray()

Creates an image from a numpy array. This is the primary bridge between Pillow and the scientific Python ecosystem.

fromarray Example
import numpy as np from PIL import Image # Create a gradient array arr = np.zeros((100, 200, 3), dtype=np.uint8) arr[:, :, 0] = np.linspace(0, 255, 200) # Red gradient img = Image.fromarray(arr) # Mode auto-detected as "RGB" img.save("gradient.png")

Instance Methods

convert()

Returns a copy of the image in another mode, such as "L" for greyscale, "RGB" to drop transparency, or "RGBA" to add an alpha channel. Converting a palette ("P") image looks each pixel up in its palette. Note that converting RGBA to RGB discards alpha rather than blending it; to put transparent images on a background, see flattening transparency.

ParameterTypeDescription
modestrThe requested mode. See Modes reference.
matrixtupleOptional 4- or 12-tuple for RGB ↔ RGB or RGB ↔ CMYK conversion matrix.
ditherDitherDithering method. NONE or FLOYDSTEINBERG
palettePalettePalette to use for P conversions. WEB or ADAPTIVE.
colorsintNumber of colors for ADAPTIVE palette. Default: 256.

save()

Writes the image to a path or file object. With a path, Pillow picks the format from the extension; with a file object, pass format=. Any extra keyword arguments (quality, optimize, exif, save_all and so on) go to that format's writer, so the options differ per format. Metadata such as EXIF is not copied unless you pass it.

ParameterTypeDescription
fpstr | Path | file-likePath or file object to save to.
formatstr | NoneOptional format override (e.g., "JPEG", "PNG").
**params—Format-specific parameters. See Image Format docs.

Transforms: resize(), resampling & thumbnail()

resize() returns a new image and defaults to BICUBIC resampling; pass Image.Resampling.LANCZOS for the highest-quality downscale. thumbnail() resizes in place and always preserves the aspect ratio, so it never upscales — a common alternative to resize() when you only want to cap dimensions. transform() defaults to NEAREST unless you pass resample.

MethodSignatureReturns
resize()resize(size, resample=BICUBIC, box=None, reducing_gap=None)Image
thumbnail()thumbnail(size, resample=BICUBIC, reducing_gap=2.0)None (in-place)
crop()crop(box=None)Image
rotate()rotate(angle, resample=NEAREST, expand=False, center=None, translate=None, fillcolor=None)Image
transpose()transpose(method)Image
transform()transform(size, method, data=None, resample=NEAREST, fill=1, fillcolor=None)Image
reduce()reduce(factor, box=None)Image

Compositing

MethodDescription
im.paste(im2, box=None, mask=None)Paste im2 into image at box. If mask is given, update only the region indicated by the mask.
im.alpha_composite(im2, dest=(0,0), source=(0,0))Composite im2 over this image in-place. Both must be RGBA.
Image.alpha_composite(im1, im2)Static version. Returns new composited image.
Image.blend(im1, im2, alpha)Return new image as: im1 * (1 - alpha) + im2 * alpha.
Image.composite(image1, image2, mask)Create composite using mask. Same as paste() but returns new image.
Compositing Example
base = Image.open("background.png").convert("RGBA") logo = Image.open("logo.png").convert("RGBA") # Resize logo to 20% of base width logo_w = base.width // 5 logo_h = int(logo.height * logo_w / logo.width) logo = logo.resize((logo_w, logo_h), Image.Resampling.LANCZOS) # Position: bottom-right corner with 20px margin pos = (base.width - logo_w - 20, base.height - logo_h - 20) base.paste(logo, pos, mask=logo) base.save("watermarked.png")

Pixel Access

MethodDescription
im.load()Allocates storage and returns a PixelAccess object for fast pixel read/write. Preferred for bulk operations.
im.getpixel(xy)Returns the pixel value at the given position. For RGB images, returns a tuple (R, G, B).
im.putpixel(xy, value)Sets pixel value at position. Very slow in a loop — use load() instead.
im.histogram()Returns a list of pixel counts per level. Length = 256 for L mode, 768 for RGB.
im.getcolors(maxcolors=256)Returns a list of (count, color) tuples. Returns None if there are more colors than maxcolors.

putpalette() & 8-Bit Palettes

Attaches a raw palette to an 8-bit image (mode "P" or "L"). The palette sequence must contain 768 integer values (256 RGB triplets: [R0, G0, B0, R1, G1, B1, ...]) or 1024 values for RGBA.

Custom Palette Example
from PIL import Image # Create a 256x256 grayscale gradient image in mode "P" img = Image.new("P", (256, 256)) for y in range(256): for x in range(256): img.putpixel((x, y), x) # Generate a 256-color heat-map palette: 768 integers (R, G, B * 256) palette = [] for i in range(256): palette.extend((i, 255 - i, (i * 2) % 256)) img.putpalette(palette) img.save("heatmap_palette.png")

draft() & Fast Decoding

Configures the file reader to load a downscaled version of the image directly from the native C decoder (currently supported for JPEG and MPO). This avoids allocating full-size RAM buffers, delivering up to 5×–10× faster load times for thumbnails.

Fast Draft Loading
from PIL import Image # Open a 48MP raw JPEG photo without allocating 200MB memory img = Image.open("huge_48mp_photo.jpg") # Ask libjpeg decoder to downscale directly at decode time img.draft("RGB", (800, 600)) img.load() # Loads fast 800x600 buffer directly into memory print(img.size) # e.g., (1000, 750) downscaled natively

Constants & Enums

ConstantValues
Image.ResamplingNEAREST, BOX, BILINEAR, HAMMING, BICUBIC, LANCZOS
Image.TransposeFLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, ROTATE_270, TRANSPOSE, TRANSVERSE
Image.TransformAFFINE, EXTENT, PERSPECTIVE, QUAD, MESH
Image.DitherNONE, FLOYDSTEINBERG, ORDERED, RASTERIZE
Image.PaletteWEB, ADAPTIVE
Image.MAX_IMAGE_PIXELSDefault: 178_956_970. Set to None to disable decompression bomb protection.

Tested examples

Everything in this section was run with Pillow 12.3.0 on Python 3.13; outputs and images are real. Sample photos: NASA/Sam Lott, public domain.

open() is lazy: see it

Opening only reads the header. Decoding happens on first use. On a 6000×4000 JPEG:

lazy_open.py
import time
from PIL import Image

Image.open("photo.jpg").resize((6000, 4000)).save("big.jpg", quality=90)

t = time.perf_counter()
img = Image.open("big.jpg")
print(f"open():  {(time.perf_counter() - t) * 1000:6.1f} ms  ->", img.format, img.size, img.mode)

t = time.perf_counter()
img.load()
print(f"load():  {(time.perf_counter() - t) * 1000:6.1f} ms  (pixels decoded now)")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
open():     0.2 ms  -> JPEG (6000, 4000) RGB
load():   119.9 ms  (pixels decoded now)

So reading the size of thousands of images is fast, as long as you do not touch the pixels.

NumPy arrays: the dtype must be right

Image.fromarray() picks the mode from the array's shape and dtype. Colour images need uint8; a float array (the default from most maths) fails:

from_numpy.py
import numpy as np
from PIL import Image

arr = np.random.rand(100, 100, 3)          # float64 values in 0..1
Image.fromarray(arr)
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback
TypeError: Cannot handle this data type: (1, 1, 3), <f8
from_numpy.py
import numpy as np
from PIL import Image

arr = np.random.default_rng(0).random((100, 100, 3))     # float64, 0..1
img = Image.fromarray((arr * 255).round().astype(np.uint8))
print(img.mode, img.size)

back = np.asarray(img)
print(back.shape, back.dtype)                             # (height, width, channels)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGB (100, 100)
(100, 100, 3) uint8

Note the axis order: NumPy arrays are (height, width, channels), while Pillow sizes are (width, height).

crop(): exclusive edges and out-of-bounds boxes

crop.py
from PIL import Image

img = Image.open("photo.jpg")                 # 1600 x 1067
box = (100, 50, 500, 350)                     # (left, upper, right, lower)
print(img.crop(box).size)                     # right and lower are exclusive: 400 x 300

edge = img.crop((1500, 1000, 1700, 1100))     # partly outside the image
print(edge.size, edge.getpixel((199, 99)))    # outside area is filled with black
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(400, 300)
(200, 100) (0, 0, 0)

rotate(): expand and fillcolor

Angles are in degrees, counter-clockwise. Without expand=True the corners are cut off:

rotate.py
from PIL import Image

img = Image.open("photo.jpg").resize((400, 267))
a = img.rotate(20)                                        # same size, corners cut off
b = img.rotate(20, expand=True, fillcolor="white")        # canvas grows to fit
print(a.size, b.size)

sheet = Image.new("RGB", (a.width + b.width + 20, b.height), "white")
sheet.paste(a, (0, (b.height - a.height) // 2))
sheet.paste(b, (a.width + 20, 0))
sheet.save("rotate.png")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
(400, 267) (468, 389)
Left: rotated image with corners cut off; right: rotated with expand=True and a white background
Real output.

For phone photos that appear sideways, do not rotate by hand: use ImageOps.exif_transpose() (see why).

convert(): what each mode looks like

The same photo converted to L, 1 with dithering, 1 without dithering and an 8-colour palette
Real output of the script below.
convert_modes.py
from PIL import Image, ImageDraw, ImageFont

src = Image.open("photo.jpg").resize((320, 213))
variants = {
    '"L"': src.convert("L"),
    '"1" (dithered)': src.convert("1"),
    '"1", dither=NONE': src.convert("1", dither=Image.Dither.NONE),
    '"P", 8 colours': src.convert("P", palette=Image.Palette.ADAPTIVE, colors=8),
}
font = ImageFont.load_default(size=18)
sheet = Image.new("RGB", (4 * 330, 250), "white")
for i, (label, im) in enumerate(variants.items()):
    sheet.paste(im.convert("RGB"), (i * 330, 0))
    ImageDraw.Draw(sheet).text((i * 330 + 160, 225), label, font=font, fill="black", anchor="mm")
sheet.save("modes.png")
Tested on ✓ Linux ✓ Windows ✓ macOS

convert("1") dithers by default (Floyd–Steinberg). For a hard threshold, pass dither=Image.Dither.NONE or use img.point(lambda v: 255 if v > 128 else 0, mode="1"); the threshold tool lets you compare methods interactively.

Reading pixels: getpixel() vs load() vs NumPy

Summing every pixel of a 1600×1067 greyscale image three ways (Pillow 12.3.0, 4-vCPU Linux VM):

pixel_speed.py
import time
import numpy as np
from PIL import Image

img = Image.open("photo.jpg").convert("L")     # 1600 x 1067 = 1.7 million pixels


def timed(label, fn):
    t = time.perf_counter(); result = fn(); print(f"{label:<22} {(time.perf_counter() - t) * 1000:8.1f} ms"); return result


def with_getpixel():
    return sum(img.getpixel((x, y)) for y in range(img.height) for x in range(img.width))

def with_load():
    px = img.load()
    return sum(px[x, y] for y in range(img.height) for x in range(img.width))

def with_numpy():
    return int(np.asarray(img, dtype=np.int64).sum())

a = timed("getpixel()", with_getpixel)
b = timed("load() pixel access", with_load)
c = timed("numpy", with_numpy)
print("same result:", a == b == c)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
getpixel()                737.0 ms
load() pixel access        92.0 ms
numpy                      64.2 ms
same result: True

getpixel() pays a method call per pixel and was about 8× slower than the pixel-access object from load(). NumPy was the fastest, and the gap grows for real per-pixel maths, where NumPy works on the whole array at once instead of looping in Python. For bulk work, convert to an array or use a built-in Pillow operation.