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)| Filter | Best for | Relative cost |
|---|---|---|
| Image.Resampling.NEAREST | Pixel art, label masks, label-preserving upscales | Cheapest |
| Image.Resampling.BILINEAR | Previews and thumbnails you will discard | Cheap |
| Image.Resampling.BICUBIC | Photographs when upscaling | Moderate |
| Image.Resampling.LANCZOS | High-quality downscaling; the default choice | Slowest of the four |
| Image.Resampling.BOX | Large downscales; averages the source pixels | Cheap |
resizetakes a target size and will happily squash the aspect ratio, so compute the other dimension yourself or useImageOps.fit.thumbnailmodifies the image in place and returnsNone; it never enlarges a small source.ImageOps.fitcrops from the centre by default; thecenteringargument takes two floats where 0.0 is the top or left edge.- On Pillow before 9.1 the filters were named
Image.LANCZOS,Image.BICUBICand the deprecated aliasImage.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.
Related
Opening, inspecting and saving images Drawing and converting formats
Last refreshed 2026-09-18.