Resizing, cropping and rotating

The geometry operations, which resampling filter to pick, and why rotate and thumbnail have surprising defaults.

Resize, thumbnail and crop

from PIL import Image, ImageOps

with Image.open("hero.jpg") as im:
    im.load()

    big = im.resize((1200, 800), Image.Resampling.LANCZOS)   # exact box, may distort
    square = ImageOps.fit(im, (512, 512), Image.Resampling.LANCZOS, centering=(0.5, 0.4))
    padded = ImageOps.pad(im, (512, 512), color="white")     # fits inside, no cropping

    thumb = im.copy()
    thumb.thumbnail((320, 320))       # in place, keeps ratio, never upscales
    print(thumb.size)

    box = (100, 80, 900, 680)         # (left, upper, right, lower); right/bottom excluded
    print(im.crop(box).size)

    square.save("square.jpg", quality=88)
FilterBest forRelative cost
Image.Resampling.NEARESTPixel art, label masks, label-preserving upscalesCheapest
Image.Resampling.BILINEARPreviews and thumbnails you will discardCheap
Image.Resampling.BICUBICPhotographs when upscalingModerate
Image.Resampling.LANCZOSHigh-quality downscaling; the default choiceSlowest of the four
Image.Resampling.BOXLarge downscales; averages the source pixelsCheap
  • resize takes a target size and will happily squash the aspect ratio, so compute the other dimension yourself or use ImageOps.fit.
  • thumbnail modifies the image in place and returns None; it never enlarges a small source.
  • ImageOps.fit crops from the centre by default; the centering argument takes two floats where 0.0 is the top or left edge.
  • On Pillow before 9.1 the filters were named Image.LANCZOS, Image.BICUBIC and the deprecated alias Image.ANTIALIAS.

Rotating, flipping and pasting

from PIL import Image

with Image.open("scan.jpg") as im:
    im.load()

    up = im.transpose(Image.Transpose.ROTATE_90)      # lossless, no interpolation
    flipped = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)

    tilted = im.rotate(12.0, resample=Image.Resampling.BICUBIC,
                       expand=True, fillcolor=(255, 255, 255))

    canvas = Image.new("RGB", (im.width + 40, im.height), "white")
    canvas.paste(im, (20, 0))
    mask = flipped.convert("L")                       # a mask must be L, 1 or RGBA
    canvas.paste(flipped, (20, im.height // 2), mask)

    canvas.save("composed.jpg", quality=90)
⚠️
Two defaults catch people out. rotate keeps the original canvas size unless you pass expand=True, so the corners of a rotated image are cut off. And thumbnail mutates the image rather than returning a new one — if you need the original, call copy() first. For pure multiples of 90 degrees use transpose, which is lossless; rotate resamples and softens the image.

FAQ

Why is my pasted image transparent or black?
The mask argument must be mode 1, L or RGBA. Passing an RGB image fails, and passing an RGBA image pastes both colour and alpha. Convert the source with .convert("L") when you only need a shape.
Should I resize before or after cropping?
Crop first whenever you can: it discards pixels you would otherwise resample, which is both faster and sharper. Resize last, so the final resampling pass produces the output resolution.

Opening, inspecting and saving images Drawing and converting formats

Last refreshed 2026-09-18.