ScreenshotNeo

BlogHow-to

How to Resize Images with Python PIL Image.open

Resize images with Pillow using exact dimensions, aspect-ratio-safe methods, quality filters, EXIF handling, and runnable Python examples.

By the ScreenshotNeo team1 October 20267 min read

Use Pillow’s Image.open() to open the source, then choose the resizing method that matches your layout requirement:

  • image.resize((width, height), Image.Resampling.LANCZOS) for exact dimensions.
  • image.thumbnail((max_width, max_height)) to fit within bounds while preserving aspect ratio.
  • ImageOps.contain, cover, fit, or pad when you need a specific fit, crop, or padding behavior.

The basic exact-size example is:

from PIL import Image

with Image.open("input.jpg") as image:
    resized = image.resize((800, 600), Image.Resampling.LANCZOS)
    resized.save("output.jpg")

resize() uses a (width, height) tuple and returns a resized copy. Pillow documents LANCZOS as a high-quality resampling choice; BICUBIC is the documented default for typical image modes. See the Pillow Image reference.

Install Pillow

python -m pip install Pillow

Check the installation:

python -c "from PIL import Image; print(Image.__version__)"

Resize to exact width and height

Pass the target width first and height second. If the source and target aspect ratios differ, the image will be stretched or squashed.

from pathlib import Path
from PIL import Image

source = Path("input.jpg")
target = Path("output.jpg")

with Image.open(source) as image:
    resized = image.resize((800, 600), resample=Image.Resampling.LANCZOS)
    resized.save(target, quality=90, optimize=True)

Use exact resizing for fixed canvases such as sprite sheets, generated thumbnails with a known crop elsewhere, or an API contract that requires precise pixel dimensions. For photographs and general downscaling, LANCZOS is a quality-oriented example. BICUBIC or BILINEAR can be reasonable alternatives when processing speed matters more.

Preserve aspect ratio

Fit inside maximum bounds with thumbnail()

from PIL import Image

with Image.open("input.jpg") as image:
    image.thumbnail((1200, 1200), resample=Image.Resampling.LANCZOS)
    image.save("thumbnail.jpg", quality=90)

thumbnail() preserves the aspect ratio and ensures neither dimension exceeds the supplied bounds. It modifies the image object in place, so copy the image first if you still need the original. Pillow’s documentation describes this mutation explicitly in the thumbnail reference.

Calculate a proportional size yourself

from PIL import Image

def resize_to_width(image, width):
    ratio = width / image.width
    height = round(image.height * ratio)
    return image.resize((width, height), Image.Resampling.LANCZOS)

with Image.open("input.jpg") as image:
    resized = resize_to_width(image, 800)
    resized.save("width-800.jpg", quality=90)

This approach is useful when the output width is fixed and the calculated height should be visible in your own code.

Choose contain, cover, crop, or padding

For a fixed rectangle without distortion, use PIL.ImageOps. The behaviors are summarized in the Pillow ImageOps reference.

Goal Method Behavior
Fit without cropping ImageOps.contain(image, size) Preserves the full image; one dimension can be smaller than the box.
Fill the box ImageOps.cover(image, size) Preserves aspect ratio; parts outside the target ratio can extend beyond the box.
Exact dimensions with a crop ImageOps.fit(image, size) Resizes and crops to the requested dimensions.
Exact dimensions with borders ImageOps.pad(image, size, color=...) Resizes and adds background space.
from PIL import Image, ImageOps

with Image.open("input.jpg") as image:
    contained = ImageOps.contain(image, (800, 600))
    contained.save("contain.jpg", quality=90)

with Image.open("input.jpg") as image:
    covered = ImageOps.cover(image, (800, 600))
    covered.save("cover.jpg", quality=90)

with Image.open("input.jpg") as image:
    cropped = ImageOps.fit(image, (800, 600), method=Image.Resampling.LANCZOS)
    cropped.save("fit.jpg", quality=90)

with Image.open("input.jpg") as image:
    padded = ImageOps.pad(image, (800, 600), color="white")
    padded.save("pad.jpg", quality=90)

Handle EXIF orientation before resizing

JPEG and TIFF files can contain EXIF orientation instructions. Apply those instructions to the pixels before measuring or resizing:

from PIL import Image, ImageOps

with Image.open("camera-photo.jpg") as image:
    oriented = ImageOps.exif_transpose(image)
    resized = oriented.resize((1200, 800), Image.Resampling.LANCZOS)
    resized.save("camera-photo-resized.jpg", quality=90)

See Pillow’s exif_transpose documentation.

Resampling filters

Filter Typical use
NEAREST Fast nearest-pixel selection; useful when discrete values must not be blended, such as pixel art or categorical masks.
BILINEAR Faster interpolation with less detail than higher-quality filters.
BICUBIC Cubic interpolation and a practical speed/quality compromise.
LANCZOS Quality-oriented downscaling, generally slower than the simpler filters.

Pillow’s filter descriptions and comparison are qualitative, not universal timing benchmarks. For mode 1 and palette mode P, Pillow forces NEAREST even if another filter is requested. Convert deliberately when smooth interpolation is required:

from PIL import Image

with Image.open("palette.png") as image:
    rgb = image.convert("RGB")
    resized = rgb.resize((800, 600), Image.Resampling.LANCZOS)
    resized.save("palette-resized.jpg", quality=90)

Transparency, color mode, and output format

Keep an alpha channel when writing PNG or WebP:

from PIL import Image

with Image.open("logo.png") as image:
    rgba = image.convert("RGBA")
    resized = rgba.resize((400, 400), Image.Resampling.LANCZOS)
    resized.save("logo-small.png")

JPEG does not support transparency. Flatten onto a chosen background before saving as JPEG:

from PIL import Image

with Image.open("logo.png") as image:
    rgba = image.convert("RGBA")
    background = Image.new("RGB", rgba.size, "white")
    background.paste(rgba, mask=rgba.getchannel("A"))
    resized = background.resize((400, 400), Image.Resampling.LANCZOS)
    resized.save("logo-small.jpg", quality=90)

PNG is suitable for lossless graphics and transparency; JPEG is commonly used for photographs; WebP can be written when your deployment supports it. Choose the format based on the consumer’s requirements rather than resizing alone.

Resize a batch of files

from pathlib import Path
from PIL import Image, ImageOps

input_dir = Path("images")
output_dir = Path("resized")
output_dir.mkdir(exist_ok=True)

for source in input_dir.glob("*.*"):
    try:
        with Image.open(source) as image:
            oriented = ImageOps.exif_transpose(image)
            oriented.thumbnail((1600, 1600), Image.Resampling.LANCZOS)
            destination = output_dir / f"{source.stem}.jpg"
            oriented.convert("RGB").save(destination, quality=88, optimize=True)
    except (OSError, ValueError) as error:
        print(f"Skipping {source}: {error}")

Performance, reliability, and cost notes

  • Downscale as early as practical to reduce memory and later processing work.
  • thumbnail() avoids enlarging an image beyond its maximum bounds; use resize() when enlargement is intentional.
  • LANCZOS trades processing speed for quality. Measure your own workload before selecting a faster filter.
  • Use the context-manager form of Image.open() so file handles are released after each image.
  • For untrusted uploads, validate file type and dimensions before processing and handle OSError cleanly.
  • Resizing locally has no API charge; the cost is CPU, memory, storage, and any infrastructure used to run the script.

Troubleshooting

ModuleNotFoundError: No module named 'PIL'

Install the Pillow package in the same Python environment that runs the script: python -m pip install Pillow. The package is named Pillow, while the import namespace is PIL.

The result has the wrong shape

Check that the tuple is (width, height). If the requested ratio differs from the source, use thumbnail(), contain, cover, fit, or pad instead of direct resize().

The original image was unexpectedly changed

thumbnail() mutates its image object. Open the file again or call image.copy() before making the thumbnail. resize() returns a separate image.

The photo is rotated after processing

Apply ImageOps.exif_transpose() before reading dimensions and resizing. The camera may have stored the orientation as metadata rather than rotating pixels.

Transparency disappeared

Keep the image in RGBA and save to PNG or a transparency-capable format. If saving JPEG, composite the alpha channel onto a background first.

Edges look jagged or details are soft

Try Image.Resampling.LANCZOS for photographic downscaling. For pixel art or masks, use NEAREST to avoid blended values.

The output file is unexpectedly large

Choose an appropriate format and, for JPEG, set an explicit quality such as 85–90. For PNG, reduce unnecessary color information when your image permits it.

Or skip the browser setup

If the image you need starts as a webpage screenshot, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. It accepts cookie banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status.

Call the API directly; see the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

ScreenshotNeo also has full-page capture with lazy images loaded, CSS-selector element capture, custom CSS and JavaScript, device presets, retina scale, waits, request blocking, headers and cookies, caching, signed links, asynchronous jobs, bulk capture, PDF output, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents.

After downloading a screenshot, resize it with the Pillow examples above. ScreenshotNeo offers 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Does Image.open() resize an image?

No. It opens and identifies the image. Call resize(), thumbnail(), or an ImageOps function on the returned object.

Which method preserves aspect ratio?

thumbnail(), contain, cover, fit, and pad preserve it. Direct resize() preserves it only when your target ratio matches the source ratio.

Can I resize without saving a file?

Yes. Keep the returned Pillow image in memory, pass it to another function, or write it to a BytesIO object instead of a filesystem path.

Should I use LANCZOS for every image?

No. It is a strong general photographic downscale choice, while NEAREST is appropriate for discrete masks and pixel art, and BILINEAR or BICUBIC may be faster for large batches.

Why does my palette image ignore the selected filter?

Pillow forces NEAREST for mode 1 and palette mode P. Convert to an appropriate color mode before resizing if smooth interpolation is required.