Reading and Writing Images in Python: JPEG, PNG, and WebP with Pillow
A code-first tour of handling JPEG, PNG, and WebP in Python with Pillow — opening, converting, compressing, and batch-processing images.
Sooner or later every Python developer has to touch images: resizing an avatar, generating thumbnails, or converting a folder of photos into a lighter format before shipping them to the web. Before writing a single line of code, though, it pays to understand what actually separates the three formats you will meet most often — JPEG, PNG, and WebP. The team over at technik-frage.de wrote a clear, beginner-friendly German primer on exactly that question, JPG, PNG oder WebP? Bildformate im Vergleich. It is the perfect conceptual companion to the practical, code-first tour we are about to take.
The three formats in a nutshell
JPEG uses lossy compression and excels at photographs, where millions of subtle color gradients make tiny compression artifacts invisible. PNG is lossless and supports transparency, which makes it the right choice for logos, icons, screenshots, and anything with sharp edges or flat color. WebP is the modern all-rounder: it offers both lossy and lossless modes plus transparency, and typically produces files 25–35% smaller than the equivalent JPEG or PNG. If you want the conceptual trade-offs spelled out in plain language, the technik-frage.de article linked above covers them well; here we focus on making Python do the work.
Installing Pillow
Pillow is the maintained fork of the venerable Python Imaging Library and the de-facto standard for image work in Python:
pip install PillowEverything below assumes from PIL import Image at the top of your file.
Opening and inspecting an image
from PIL import Image
img = Image.open("photo.jpg")
print(img.format, img.size, img.mode)
# JPEG (4032, 3024) RGBPillow reads the format from the file contents, not the extension, so a mislabeled file will still open correctly. The mode tells you how pixels are stored — RGB, RGBA (with an alpha channel), L (grayscale), and so on.
Converting between formats
Saving to a different format is as simple as changing the output extension — Pillow infers the encoder from it:
img = Image.open("logo.png")
img.save("logo.jpg") # PNG -> JPEGThere is one classic trap: JPEG has no alpha channel. If your PNG has transparency and you save it as JPEG, Pillow will complain. Flatten it onto a background first:
img = Image.open("logo.png").convert("RGBA")
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3]) # use alpha as mask
background.save("logo.jpg", quality=90)Controlling quality and file size
For JPEG, the quality parameter (1–95) is your main lever, and optimize=True squeezes out a few extra bytes:
img.save("photo.jpg", quality=82, optimize=True)Values around 80–85 are usually indistinguishable from the original while cutting file size dramatically. Pushing above 95 mostly just inflates the file.
Exporting to WebP
WebP is where you win the most on the modern web. Lossy WebP takes the same quality argument; lossless mode is a single flag:
img.save("photo.webp", quality=80) # lossy
img.save("icon.webp", lossless=True) # lossless, keeps transparencyBatch converting a folder
Putting it together, here is a small script that walks a directory and produces optimized WebP versions of every JPEG and PNG it finds:
from pathlib import Path
from PIL import Image
source = Path("images")
for path in source.glob("*"):
if path.suffix.lower() in {".jpg", ".jpeg", ".png"}:
img = Image.open(path)
out = path.with_suffix(".webp")
img.save(out, quality=80, method=6)
print(f"{path.name} -> {out.name}")The method=6 argument tells the WebP encoder to try harder for a smaller file at the cost of a little CPU — a good trade when you are converting once and serving many times.
Wrapping up
Once you understand what each format is good at — the conceptual half covered nicely in technik-frage.de's Bildformate im Vergleich — Pillow makes the practical half almost trivial. Open, convert, tune the quality, and batch the whole thing in a dozen lines. Reach for PNG when you need crisp edges and transparency, JPEG for photographs headed somewhere that does not yet speak WebP, and WebP whenever you control the delivery pipeline and want the smallest bytes on the wire.