How to use PIL in Python

Learn to use the Python Imaging Library (PIL). This guide covers different methods, tips, real-world applications, and how to debug errors.

How to use PIL in Python
Published on: 
Wed
Mar 25, 2026
Updated on: 
Thu
Mar 26, 2026
The Replit Team

The Python Imaging Library, or Pillow, is an essential tool for image processing. It lets you open, manipulate, and save various image formats with simple, powerful functions.

In this article, you'll learn core techniques and practical tips for effective image manipulation. You will explore real-world applications and receive debugging advice to help you confidently master Pillow for your projects.

Opening and displaying an image with PIL

from PIL import Image

# Open an image file
img = Image.open('sample.jpg')

# Display basic information about the image
print(f"Format: {img.format}, Size: {img.size}, Mode: {img.mode}")

# Show the image
img.show()--OUTPUT--Format: JPEG, Size: (800, 600), Mode: RGB

The Image.open() function is your entry point. It creates an Image object that holds the image's pixel data and metadata, which you'll use for all subsequent manipulations. This is more than just opening a file; it's loading it into a structure ready for processing.

Before you start editing, it's crucial to inspect the image's properties. Key attributes include:

  • format: The original file format, like JPEG or PNG.
  • size: The image dimensions in pixels (width, height).
  • mode: The pixel format, such as RGB (color) or L (grayscale), which determines how you can process the image.

The show() method then opens the image in your system's default viewer, offering a simple way to preview your work.

Basic image operations

With your image loaded, you can now perform fundamental edits like resizing with the resize() method, cropping with crop(), and adjusting its orientation.

Resizing images with resize() method

from PIL import Image

img = Image.open('sample.jpg')
resized_img = img.resize((400, 300))
print(f"Original size: {img.size}")
print(f"Resized: {resized_img.size}")
resized_img.save('resized_sample.jpg')--OUTPUT--Original size: (800, 600)
Resized: (400, 300)

The resize() method creates a new image with your desired dimensions. It's non-destructive, meaning it returns a new Image object and leaves the original untouched. This is why you assign the result to a new variable like resized_img.

  • You provide the new dimensions as a tuple: (width, height).

Finally, you must call the save() method on the new image object to write your changes to a file.

Cropping images using the crop() function

from PIL import Image

img = Image.open('sample.jpg')
# Crop parameters: (left, upper, right, lower)
cropped_img = img.crop((100, 100, 500, 400))
print(f"Cropped dimensions: {cropped_img.size}")
cropped_img.save('cropped_sample.jpg')--OUTPUT--Cropped dimensions: (400, 300)

The crop() method extracts a rectangular portion of an image. You define this area by passing a 4-element tuple representing the pixel coordinates of the crop box.

  • left: The x-coordinate for the left edge.
  • upper: The y-coordinate for the top edge.
  • right: The x-coordinate for the right edge.
  • lower: The y-coordinate for the bottom edge.

Pillow’s coordinate system starts at (0,0) in the top-left corner. The new image’s dimensions will be the difference between these coordinates, creating a cropped view of the original.

Rotating and flipping images

from PIL import Image

img = Image.open('sample.jpg')
rotated_img = img.rotate(45)
flipped_img = img.transpose(Image.FLIP_LEFT_RIGHT)
print("Image rotated and flipped version created")
rotated_img.save('rotated_sample.jpg')
flipped_img.save('flipped_sample.jpg')--OUTPUT--Image rotated and flipped version created

Adjusting an image’s orientation is straightforward. You can use the rotate() method for custom angle rotations or the transpose() method for standard flips and 90-degree turns.

  • The rotate() method accepts an angle in degrees and rotates the image counter-clockwise. The example uses rotate(45) to tilt the image.
  • The transpose() method handles precise flips. By passing a constant like Image.FLIP_LEFT_RIGHT, you can mirror the image horizontally.

Advanced PIL techniques

Beyond basic transformations, Pillow lets you apply artistic filters, draw custom text and shapes, and optimize images for different formats and performance.

Applying filters and enhancements

from PIL import Image, ImageFilter, ImageEnhance

img = Image.open('sample.jpg')
blurred = img.filter(ImageFilter.BLUR)
sharpened = img.filter(ImageFilter.SHARPEN)
enhancer = ImageEnhance.Contrast(img)
enhanced = enhancer.enhance(1.5)
print("Applied blur, sharpen, and contrast enhancement")--OUTPUT--Applied blur, sharpen, and contrast enhancement

Pillow's ImageFilter and ImageEnhance modules unlock powerful editing capabilities. You can apply standard filters directly with the filter() method.

  • The ImageFilter module provides ready-to-use filters like ImageFilter.BLUR and ImageFilter.SHARPEN.
  • For more granular control, the ImageEnhance module lets you adjust properties like contrast. You first create an enhancer object—for example, ImageEnhance.Contrast(img)—and then call its enhance() method with a factor to specify the intensity. A factor of 1.0 means no change, while values greater than 1.0 increase the effect.

Drawing shapes and text on images

from PIL import Image, ImageDraw, ImageFont

img = Image.new('RGB', (400, 200), color='white')
draw = ImageDraw.Draw(img)
draw.rectangle([(50, 50), (350, 150)], outline='red', width=3)
draw.ellipse([(100, 75), (300, 125)], fill='blue')
font = ImageFont.truetype('arial.ttf', 20)
draw.text((150, 90), "Hello PIL!", fill='white', font=font)
img.save('drawing_sample.jpg')--OUTPUT--# (Image with a red rectangle, blue ellipse, and "Hello PIL!" text)

To add custom graphics, you'll use the ImageDraw module. First, create a drawing context by passing your image to ImageDraw.Draw(). This gives you an object that lets you draw directly onto the image canvas. While the example creates a new blank image with Image.new(), you can also draw on any image you've opened.

  • Shape-drawing methods like rectangle() and ellipse() take coordinates to define the area, along with parameters like outline and fill for styling.
  • To add text, you first load a font using ImageFont.truetype(). Then, you can use the draw.text() method to position and write your string on the image.

Converting between formats and optimizing

from PIL import Image

img = Image.open('sample.jpg')
# Convert to PNG
img.save('sample.png')
# Convert to WebP with quality settings
img.save('sample.webp', 'WEBP', quality=80)
# Create a thumbnail (preserves aspect ratio)
img.thumbnail((200, 200))
print(f"Thumbnail size: {img.size}")
img.save('thumbnail_sample.jpg', optimize=True, quality=85)--OUTPUT--Thumbnail size: (200, 150)

Pillow makes format conversion and optimization straightforward. You can convert an image simply by changing the file extension when calling the save() method. For modern formats like WebP, you can also pass specific arguments, such as quality, to fine-tune the output.

  • The thumbnail() method creates a smaller version of your image. Unlike resize(), it preserves the aspect ratio and modifies the image object in-place.
  • When saving, you can include parameters like optimize=True and quality to reduce file size effectively.

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.

For the image manipulation techniques we've explored, Replit Agent can turn them into production-ready tools. You can build complete applications that leverage Pillow's capabilities directly from a simple description.

  • Build an automatic thumbnail generator that resizes uploaded images to predefined dimensions using the thumbnail() method.
  • Create a profile picture editor that lets users crop their photos into a perfect square and apply filters like ImageFilter.BLUR.
  • Deploy a watermarking utility that uses ImageDraw to add custom text overlays to a batch of images.

Describe your app idea, and the agent writes the code, tests it, and fixes issues automatically. Try Replit Agent to bring your image processing ideas to life.

Common errors and challenges

While Pillow is powerful, you'll likely encounter a few common issues; here’s how to navigate them with confidence.

Handling "File not found" errors with Image.open()

A frequent hurdle is the FileNotFoundError when using Image.open(). This error almost always means the path to your image is incorrect or the file isn't where your script expects it to be. Before you do anything else, double-check your file paths.

  • Make sure the file name and extension are spelled correctly.
  • Verify that the file is in the same directory as your script, or provide a full, absolute path to its location.

Correctly pasting transparent images with the paste() method

Pasting transparent images, like PNGs, can be tricky. If you use the paste() method without accounting for transparency, you might see an unwanted solid background instead of a clean overlay. The key is to use the image's own alpha channel—the part that stores transparency information—as a mask.

  • When calling paste(), you can pass the transparent image itself as an optional third argument. This tells Pillow to use the image's alpha channel to blend it correctly, preserving its transparency.

Managing memory when processing large images with thumbnail()

Working with large, high-resolution images can quickly exhaust your system's memory. Because Pillow loads the entire uncompressed image data, a single file can consume hundreds of megabytes, which is a major problem when processing many images.

  • Prefer the thumbnail() method over resize() when creating smaller versions. It's often more memory-efficient because it modifies the image in-place, avoiding the need to hold two large images in memory at once.
  • When processing a batch of images, it's best to open, process, and save each one individually. Be sure to close the image object after you're done to release its memory before moving on to the next.

Handling "File not found" errors with Image.open()

The FileNotFoundError will stop your script cold. It’s triggered when Image.open() can’t find the file you specified, usually because of a simple pathing issue or a typo in the filename. It's a frustrating but fixable problem.

See what happens when the code tries to open a nonexistent file.

from PIL import Image

# This will crash if the file doesn't exist
img = Image.open('nonexistent_image.jpg')
img.show()

The script crashes because it tries to open nonexistent_image.jpg without first confirming the file is actually there. This direct call to Image.open() is what triggers the error. See how to handle this situation gracefully in the code below.

from PIL import Image
import os

filename = 'nonexistent_image.jpg'
if os.path.exists(filename):
img = Image.open(filename)
img.show()
else:
print(f"Error: File '{filename}' not found")

To prevent a FileNotFoundError, you can check if a file exists before calling Image.open(). The solution uses Python's built-in os module to perform this check.

  • The os.path.exists() function returns True if the file is found, allowing your script to proceed.
  • If it returns False, you can handle the error gracefully—like printing a message—instead of letting the program crash. This is crucial when working with user-provided file paths.

Correctly pasting transparent images with the paste() method

Pasting a transparent image, like a logo, onto a background often goes wrong. If you don't handle the alpha channel correctly, the paste() method ignores transparency, leaving an unwanted solid box. The code below shows this common mistake in action.

from PIL import Image

background = Image.new('RGB', (400, 300), color='blue')
overlay = Image.open('transparent_logo.png') # RGBA image
background.paste(overlay, (50, 50)) # Won't handle transparency
background.save('composite.jpg')

Calling paste() with just two arguments ignores the overlay's alpha channel, causing the unwanted solid box. The method needs a third argument—a mask—to blend the images correctly. The following code demonstrates the proper approach.

from PIL import Image

background = Image.new('RGB', (400, 300), color='blue')
overlay = Image.open('transparent_logo.png') # RGBA image
if overlay.mode == 'RGBA':
background.paste(overlay, (50, 50), overlay) # Use alpha as mask
else:
background.paste(overlay, (50, 50))
background.save('composite.jpg')

The solution is to use the three-argument version of the paste() method. By passing the overlay image itself as the third argument, you tell Pillow to use its alpha channel as a mask. This ensures the transparent areas blend seamlessly with the background, solving the solid box issue.

  • It’s smart to first check if the image mode is RGBA. This makes your code robust enough to handle images that don't have transparency.

Managing memory when processing large images with thumbnail()

Processing large images can quickly exhaust your system's memory. Using methods like resize() is a common culprit because it creates a new, large image object in memory alongside the original, which can cause performance issues. The code below shows this memory-intensive operation in action.

from PIL import Image

img = Image.open('very_large_image.jpg')
processed = img.resize((img.width // 2, img.height // 2))
processed.save('resized_large_image.jpg')

This code creates a new processed object with resize(), forcing the script to hold two large images in memory. This inefficient approach risks memory exhaustion with large files. Observe how the following code handles this more efficiently.

from PIL import Image

img = Image.open('very_large_image.jpg')
img.thumbnail((img.width // 2, img.height // 2))
img.save('resized_large_image.jpg')
del img # Explicitly free the memory

The thumbnail() method is a more memory-efficient alternative to resize() because it modifies the image object in-place. This means you don't need to hold two large images in memory at once, which is crucial when processing high-resolution files or large batches. After saving, explicitly deleting the image object with del img helps release memory immediately. This simple practice prevents potential crashes or slowdowns and keeps your script lean and performant.

Real-world applications

Beyond troubleshooting common errors, you can now apply these skills to build practical tools for watermarking and batch processing images.

Adding text watermarks using the ImageDraw module

Adding a text watermark is a practical way to protect your images, and you can do this by using the ImageDraw module to render text directly onto the image.

from PIL import Image, ImageDraw, ImageFont

img = Image.open('sample.jpg')
draw = ImageDraw.Draw(img)
font = ImageFont.truetype('arial.ttf', 36)
draw.text((10, img.height - 50), "© Copyright 2023", fill=(255, 255, 255), font=font)
img.save('watermarked_image.jpg')
print("Watermark added to the image")

This script adds text directly onto an image by creating a drawing context with ImageDraw.Draw(img). This step essentially gives you a canvas to work on.

  • First, you load a font and size using ImageFont.truetype().
  • Next, the draw.text() method writes your string onto the image. The position is dynamically calculated using img.height - 50, ensuring the text appears consistently near the bottom left.
  • Finally, save() writes the modified image to a new file, preserving your original.

Batch processing images with the thumbnail() method

Automating image resizing for an entire folder is a common task, and you can accomplish it efficiently by looping through your files and applying the thumbnail() method to each one.

import os
from PIL import Image

input_dir = "photos"
output_dir = "thumbnails"
os.makedirs(output_dir, exist_ok=True)

for filename in os.listdir(input_dir):
if filename.endswith('.jpg'):
img = Image.open(os.path.join(input_dir, filename))
img.thumbnail((200, 200))
img.save(os.path.join(output_dir, filename))

print(f"Created thumbnails for all images in {input_dir}")

This script combines file system operations with image processing to create thumbnails in bulk. It starts by ensuring an output_dir exists using os.makedirs(), then iterates through every item in the input_dir.

  • It uses a conditional check with endswith() to process only JPEG files.
  • For each valid image, it generates a thumbnail that preserves the original aspect ratio and saves the new file to the output directory, automating an otherwise tedious task.

Get started with Replit

Now, turn these techniques into a real tool. Tell Replit agent: "Build a web app that adds a text watermark to uploaded images" or "Create a utility that batch-converts images to optimized thumbnails."

The agent writes the code, tests for errors, and deploys your app from a simple prompt. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.