Advertisement
Open Source Projects by Phil Schwartz

How I Created a Python Script to Batch-Resize Images with Pillow

Resizing a folder of images by hand is manageable until a project produces hundreds of files. Product photographs, screenshots, scanned documents and web graphics often arrive at different dimensions, file sizes and colour profiles. A small Python utility can turn that repetitive task into a predictable command-line workflow.

I built this image batch-processing script around Pillow, the actively maintained imaging library for Python. The goal was straightforward: read images from one directory, scale them to fit within a chosen maximum size, preserve their proportions, and save the results elsewhere without overwriting the originals.

Defining the resizing workflow

Before writing code, I decided what “resize” should mean. Stretching every file to exactly 1200 × 800 pixels would distort portrait photographs and square logos. Instead, the script should constrain the longest dimensions while retaining each image’s aspect ratio.

The input and output directories are separate for safety. That matters when processing a client’s product catalogue or a personal photo archive stored on a laptop in Melbourne. A mistake should create an unwanted output file, not destroy the source image. The script also creates the destination directory automatically, so it works cleanly in a fresh project or a scheduled job.

The basic command-line interface accepts a source folder, destination folder, and maximum width and height:

python resize_images.py originals resized --max-width 1600 --max-height 1200

This design keeps the utility useful for different jobs. A photographer might prepare large JPEGs for online proofing, while an Australian retailer might create smaller product images for a Shopify or WooCommerce catalogue. The same program can handle both cases without editing its source code.

Building the Pillow image processor

Pillow provides the operations needed for this utility: opening files, reading their dimensions, applying a high-quality resampling filter and saving the result. The thumbnail() method is particularly convenient because it modifies the image in place while preserving its aspect ratio.

A practical version of the processing loop looks like this:

from pathlib import Path
from PIL import Image, UnidentifiedImageError

SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"}

def resize_images(source_dir, output_dir, max_size):
    source = Path(source_dir)
    output = Path(output_dir)
    output.mkdir(parents=True, exist_ok=True)

    for image_path in source.iterdir():
        if not image_path.is_file():
            continue
        if image_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
            continue

        destination = output / image_path.name

        try:
            with Image.open(image_path) as image:
                image.thumbnail(max_size, Image.Resampling.LANCZOS)
                image.save(destination)
                print(f"Saved {destination}")
        except (UnidentifiedImageError, OSError) as error:
            print(f"Skipped {image_path}: {error}")

Image.Resampling.LANCZOS produces sharp results when reducing photographs, although it can make processing slower than a lower-quality filter. That trade-off is usually worthwhile for web assets and printed material. The context manager also ensures that file handles are closed promptly, which becomes important when a directory contains thousands of images.

Handling formats, metadata and filenames

JPEG, PNG, WebP and TIFF do not behave identically. A PNG may contain transparency, while a JPEG cannot store an alpha channel. If a script converts every file to JPEG without checking its colour mode, an RGBA image can trigger an error or lose its transparent background.

For a general-purpose tool, preserving the original extension is the least surprising choice. More specialised workflows can add explicit conversion rules, such as flattening transparent PNGs onto a white background before saving JPEG output. It is also worth deciding whether EXIF metadata should be retained. Pillow may not preserve every metadata field automatically, so sensitive location data should be considered before distributing the resized files.

That issue has practical importance in Australia. A photo taken around Sydney or the Blue Mountains can contain GPS coordinates in its EXIF data, and the Privacy Act 1988 may be relevant when an organisation handles personal information. Resizing is therefore a good point at which to review metadata, especially when publishing staff photographs, customer uploads or images containing identifiable people.

Filenames need similar care. Simple scripts can overwrite files when two directories contain names such as image.jpg and image.JPG, or when a source contains duplicate exports. A production version can generate unique names, retain directory structure, and write a processing log. Those features are less glamorous than the resizing algorithm, but they make the utility safer to use repeatedly.

Making the script reliable at scale

Batch processing should continue when one image is corrupt. The exception handler in the example reports the problem and moves to the next file instead of terminating the entire run. This is useful for archives collected from USB drives, network shares or older Linux systems, where an incomplete download may sit among otherwise valid images.

I also prefer explicit progress messages and a final count of processed, skipped and failed files. A command-line tool gives the operator evidence that it actually handled the directory. For a larger utility, Python’s logging module can write timestamps and error details to a log file, while argparse can validate dimensions and provide built-in help.

Reliability is a broader software concern than image manipulation. In another Python project, I had to investigate a race-condition debugging story involving concurrent updates. The same habit applies here: define the file lifecycle, consider partial failures, and make repeated runs predictable. Even a small script benefits from thinking about what happens when an operation is interrupted halfway through.

Testing and extending the utility

I test a batch-resize program with landscape, portrait and square images, along with transparent PNGs, unusually large TIFF files and invalid extensions. I compare the output dimensions rather than relying only on visual inspection. Every result should fit within the maximum bounds, and no result should be enlarged accidentally unless that behaviour is explicitly requested.

Australian internet conditions also influence practical testing. A small business in regional Queensland may upload images over a slower connection than a studio in Melbourne, so reducing file size can improve publishing workflows. However, a lower pixel count is not always enough: JPEG quality, WebP conversion and metadata removal can make a substantial difference to upload times and storage costs.

Useful extensions include a --quality option, recursive directory traversal, date-based output folders and a dry-run mode. A dry run can list which files would change without writing anything, which is valuable before processing a shared photo archive. Unit tests can isolate dimension calculations, while integration tests can create temporary directories and verify the generated files.

The finished script remains deliberately modest. It avoids a graphical interface and external services, runs on Linux or other Python-supported platforms, and can be adapted as requirements change. That simplicity is the main advantage: a repeatable Pillow command replaces a tedious sequence of manual edits while leaving the original images safely untouched.