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.

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 asRGB(color) orL(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 usesrotate(45)to tilt the image. - The
transpose()method handles precise flips. By passing a constant likeImage.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
ImageFiltermodule provides ready-to-use filters likeImageFilter.BLURandImageFilter.SHARPEN. - For more granular control, the
ImageEnhancemodule lets you adjust properties like contrast. You first create an enhancer object—for example,ImageEnhance.Contrast(img)—and then call itsenhance()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()andellipse()take coordinates to define the area, along with parameters likeoutlineandfillfor styling. - To add text, you first load a font using
ImageFont.truetype(). Then, you can use thedraw.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. Unlikeresize(), it preserves the aspect ratio and modifies the image object in-place. - When saving, you can include parameters like
optimize=Trueandqualityto 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
ImageDrawto 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 overresize()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 returnsTrueif 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 usingimg.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.
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.
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.

.png)
.png)
.png)