LESSON 148 · PRACTICAL PYTHON30 min

Pillow Resize Image Without Losing Aspect Ratio

Resize an image into a maximum width and height without distortion, upscaling, or changing the original image.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Pillow Resize Image Without Losing Aspect Ratio

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Someone searches for how to resize an image with Pillow and usually wants a photo that fits a maximum size without looking stretched. A square output sounds convenient for a thumbnail, but turning a 1200 by 800 landscape image directly into 600 by 600 changes its shape. On a real photo, a circle becomes an oval and a face looks wrong. We will use an in-memory image so this lesson does not need access to your personal files. We will first reproduce the geometry mistake, then calculate one scale factor for both dimensions. That single number is the heart of aspect- ratio preservation.

  2. 00:40

    We will also decide whether a small source should be enlarged, check what Pillow's `thumbnail` method mutates, and finish with portrait and boundary cases. The practical page includes code you can run in a browser; for a local script, install Pillow in your Python environment with `python -m pip install Pillow`. The code runs cleanly because Pillow has done exactly what we requested. Resize takes an exact pixel width and height; it does not infer that 600 by 600 was only a bounding box. The source ratio is one point five. The output ratio is one. That change is the distortion, even though both dimensions became smaller.

  3. 01:22

    The mistake is easy to miss when a sample is a solid color or a screenshot is viewed at a small size, so inspect dimensions before looking only at the visual output. A real image may also have an EXIF orientation tag. For photographs, apply `ImageOps.exif_transpose` before measuring displayed dimensions. The in-memory fixture has no EXIF metadata, letting us focus on the resize calculation. We should keep the original source object intact because the same file may need a full-resolution version or another thumbnail later. The `resize` call returns a new object here, but there is another Pillow method

  4. 02:02

    with different mutation behavior. Pillow's thumbnail method interprets 300 by 300 as a maximum box and preserves the original proportions. A 1200 by 800 landscape image becomes 300 by 200. That is a very useful shortcut when fitting inside a box is the whole requirement. But thumbnail changes the image object on which it is called, so we made a copy first. The original stays 1200 by 800. This difference between resize returning a new image and thumbnail modifying its receiver matters when several versions of one photo are needed.

  5. 02:43

    We selected `Image.Resampling.LANCZOS` for quality photographic downscaling. A pixel-art sprite may instead need nearest-neighbor scaling to keep hard edges. The filter controls how pixels are sampled; it cannot repair a wrong aspect ratio. We still want to implement the math ourselves for the exercise, because it reveals how to avoid upscaling and how to reject invalid bounds deliberately. A library convenience call is useful, but understanding its contract helps us choose it correctly. We divide the allowed width by source width and allowed height by source height. The smaller ratio is the limiting dimension.

  6. 03:24

    For this landscape input, the width limit wins: 600 divided by 1200 equals one half. Multiplying both source dimensions by one half gives 600 by 400. The final candidate, one, prevents upscaling. If the image already fits the box, the scale remains one and its dimensions remain unchanged. We validate that both limits are positive before dividing. Rounding turns computed dimensions into pixel integers; `max(1, ...)` keeps a very narrow source from reaching zero pixels. Pillow then creates a new image with LANCZOS resampling, leaving the original object alone.

  7. 04:06

    This function makes a specific product decision: fit inside the box without enlarging. Another product might want a filled 600 by 600 square, which requires cropping or padding, not stretching. For real files, open and save with explicit paths and formats. The lesson's function works on an image object so that the geometry is testable without disk access. A portrait is limited by height, so its 900 pixels become 400 and the width rounds to 133. The source remains 300 by 900. A 20 by 10 image already fits and is not enlarged. An invalid zero width produces a clear ValueError instead of division by zero or

  8. 04:49

    an impossible image size. These are stronger checks than looking at one landscape sample. When using a file from a camera, remember that orientation metadata can change what the viewer perceives as width and height. `ImageOps.exif_transpose` should be applied before sizing a real oriented photo. Also choose the output format consciously: a transparent RGBA image cannot simply be saved as JPEG without deciding how to handle alpha. Resizing pixels does not guarantee a particular file size in bytes, because encoding and quality settings matter too.

  9. 05:30

    On the page, run the examples, complete the `fit_within` task, and compare the printed dimensions with the source. The observable contract is a proportional result inside the bounds and an untouched original.

Understand the concept

The long-tail question 'pillow resize image' usually means fitting a photo to a target size without stretching it. Image.resize((width, height)) uses the exact dimensions supplied; if their ratio differs from the source ratio, circles become ovals and faces look distorted. Calculate one scale factor for both dimensions, or use thumbnail on a copy when a bounded image is all you need.

This tutorial creates sample images in memory with Pillow, so no personal photo or local path is required to practice. For a real file, install Pillow in local Python with `python -m pip install Pillow`, open it with Image.open inside a with block, and save a resized copy. Pillow is a third-party package. The browser console loads its supported Pyodide package before running the examples; a local script needs a local installation. The examples print dimensions rather than uploading image bytes.

Choose scale = min(max_width / image.width, max_height / image.height, 1). The final 1 prevents accidental upscaling. Apply that single factor to both dimensions and clamp rounding to at least one pixel for a very narrow source. Validate positive limits before dividing. Image.Resampling.LANCZOS is a high-quality filter for downscaling photographs; nearest-neighbor can be more appropriate for hard-edged pixel art. resize returns a new image, leaving the original untouched.

A local photograph can contain an EXIF orientation tag. Use ImageOps.exif_transpose before measuring its displayed width and height, then save the resulting image with a deliberate format. JPEG does not support alpha transparency, so decide how to handle RGBA input before saving JPEG. For bulk files, preserve paths safely and verify outputs visually. This lesson focuses on the dimension calculation; it does not pretend that a resized pixel count guarantees a target file size.

  • resize uses exact dimensions and can distort
  • One scale factor preserves aspect ratio
  • A bounding box and no-upscale rule are separate choices
  • Image.Resampling.LANCZOS controls downsampling quality

See it step by step

Read the code, predict the output, then compare it with the result.

01. See why exact dimensions can stretch

PYTHON
from PIL import Image
source = Image.new('RGB', (1200, 800), 'navy')
wrong = source.resize((600, 600))
print(source.size, wrong.size)
print(source.width / source.height, wrong.width / wrong.height)
EXPECTED OUTPUT
(1200, 800) (600, 600)
1.5 1.0

The requested square has a different ratio from the source.

02. Use thumbnail on a copy

PYTHON
from PIL import Image
source = Image.new('RGB', (1200, 800), 'navy')
copy = source.copy()
copy.thumbnail((300, 300), resample=Image.Resampling.LANCZOS)
print(copy.size, source.size)
EXPECTED OUTPUT
(300, 200) (1200, 800)

thumbnail fits the box but mutates its receiver, so copy first.

03. Fit without upscaling

PYTHON
from PIL import Image
def fit(image, width, height):
    scale = min(width / image.width, height / image.height, 1)
    size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
    return image.resize(size, Image.Resampling.LANCZOS)
print(fit(Image.new('RGB', (1200, 800)), 600, 600).size)
print(fit(Image.new('RGB', (200, 100)), 600, 600).size)
EXPECTED OUTPUT
(600, 400)
(200, 100)

The same scale controls both axes, and a smaller image remains at its original size.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Measure before resizing

The page targets a specific image-processing need: put a source inside a maximum width and height while retaining its shape. Resize accepts exact dimensions and does not decide which ratio the caller intended. Print source.size first and compare width divided by height before selecting new dimensions.

The in-memory sample is a landscape image of 1200 by 800 pixels. Resizing it to 600 by 600 halves the width but only reduces the height by one quarter. The resulting square has ratio 1.0 instead of 1.5. Pixel colors cannot repair this geometry after the fact.

PYTHON
from PIL import Image
source = Image.new('RGB', (1200, 800), 'navy')
wrong = source.resize((600, 600))
print(source.size, wrong.size)
print(source.width / source.height, wrong.width / wrong.height)
EXPECTED OUTPUT
(1200, 800) (600, 600)
1.5 1.0
Follow the reasoning
  1. Run the square resize and print both ratios.
  2. Describe the distortion before looking at the corrected result.
02 / 03

Fit inside a maximum box

Divide each allowed dimension by the corresponding source dimension, then take the smaller ratio. Applying that ratio to both axes keeps proportions. Include 1 in the minimum when the product requirement says a small image should not be enlarged. Validate positive bounds before performing the divisions.

Pillow's Image.Resampling.LANCZOS gives a good filter for photographic downscaling. resize returns a new image while thumbnail modifies the receiver. If a thumbnail is convenient, call it on image.copy() so the original full-resolution image is available for another output.

PYTHON
from PIL import Image
source = Image.new('RGB', (1200, 800), 'navy')
copy = source.copy()
copy.thumbnail((300, 300), resample=Image.Resampling.LANCZOS)
print(copy.size, source.size)
EXPECTED OUTPUT
(300, 200) (1200, 800)
Follow the reasoning
  1. Run the copy-and-thumbnail example.
  2. Compare the copy and source sizes to identify which object changed.
03 / 03

Check orientation and edge sizes

The task uses one scale, rounds each dimension, and clamps the result to at least one pixel. A portrait image should use the height limit while a wide image should use the width limit. A source already smaller than both bounds should remain the same size and still yield a separate result object.

Real camera files may have an EXIF orientation that changes the displayed geometry. Call ImageOps.exif_transpose before measuring a file-backed photo. When saving, choose a format deliberately; JPEG cannot represent alpha transparency. These file concerns are separate from the in-memory dimension calculation tested in the browser.

PYTHON
from PIL import Image
def fit(image, width, height):
    scale = min(width / image.width, height / image.height, 1)
    size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
    return image.resize(size, Image.Resampling.LANCZOS)
portrait = Image.new('RGB', (300, 900))
print(fit(portrait, 600, 400).size, portrait.size)
print(fit(Image.new('RGB', (20, 10)), 600, 400).size)
EXPECTED OUTPUT
(133, 400) (300, 900)
(20, 10)
Follow the reasoning
  1. Run the fit function for a portrait and a small source.
  2. Check both result sizes and confirm that the original remains unchanged.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete fit_within(image, max_width, max_height). Preserve the image ratio, never enlarge a smaller source, return a new image, and reject nonpositive bounds. The 1200×800 sample in a 600×600 box must print (600, 400) (1200, 800).
Need a hint?

Use one scale: min(max_width / image.width, max_height / image.height, 1). Round both scaled dimensions, clamp them to 1 pixel, and call image.resize with Image.Resampling.LANCZOS.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What happens when resize receives an unrelated width and height?

    02.Why include 1 in min(width_ratio, height_ratio, 1)?

    03.Which filter is used for quality downscaling here?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Forcing the bounding-box dimensions

    COMMON MISTAKE
    image.resize((600, 600))
    FIX
    image.resize((600, 400), resample=Image.Resampling.LANCZOS)

    What happens: A 1200×800 image becomes a distorted square.
    Calculate a matching size from one scale factor.

    Mutating the only source with thumbnail

    COMMON MISTAKE
    image.thumbnail((600, 600))
    FIX
    copy = image.copy(); copy.thumbnail((600, 600))

    What happens: The original object is changed in place.
    Use a copy when the full-resolution source must remain available.

    Browse this lesson’s problem fixes

    Sources and further reading