Opening, inspecting and saving images
What Image.open actually does, how to read a file's real properties, and how to save in a format that supports the modes you have.
Opening is lazy
Image.open reads the header only. Pixels are decoded when you first use them, which is why the returned object is cheap to create and why the underlying file must stay open until you touch the data.
from PIL import Image, ImageOps
with Image.open("photo.jpg") as im:
print(im.format, im.size, im.mode) # JPEG (4032, 3024) RGB
print(im.getbands()) # ('R', 'G', 'B')
im.load() # decode the pixels while the file is open
exif = im.getexif()
print(exif.get(274)) # 274 is the Orientation tag
upright = ImageOps.exif_transpose(im) # apply the rotation, drop the tag
print(upright.size)
upright.save("upright.jpg", quality=90)
# after the with block the pixels are already in memory, so 'upright' is still usableim.sizeis a(width, height)tuple;cropandpastetake coordinates in that same order.im.modetells you what the pixel data means — the most useful single property when a save fails.im.infoholds metadata the decoder captured (DPI, ICC profile, EXIF, animation frames); it is not necessarily written back on save.- Phones store a landscape image plus an orientation tag, so a photo can appear rotated until
ImageOps.exif_transposeis applied. - For multi-frame files (GIF, TIFF) use
im.n_framesandim.seek(n);ImageSequence.Iteratorwraps that loop for you.
Saving and format support
from PIL import Image
im = Image.open("photo.jpg").convert("RGB")
# the format comes from the extension when you pass a path
im.save("out.jpg", quality=85, optimize=True, progressive=True)
im.save("out.webp", quality=80, method=6) # method 0-6: slower means smaller
im.save("out.png", optimize=True) # PNG is lossless; quality is ignored
# for a file object you must state the format explicitly
with open("stream.jpg", "wb") as fh:
im.save(fh, format="JPEG", quality=85)| Format | Alpha | Lossy | Notes |
|---|---|---|---|
| JPEG | No | Yes | quality 1-95 (75 default); progressive helps the web |
| PNG | Yes | No | optimize=True; excellent for screenshots, poor for photos |
| WebP | Yes | Both | lossless=True switches mode; method controls encode effort |
| GIF | 1-bit | Palette | save_all=True for animation, loop=0 for forever |
| TIFF | Yes | No by default | compression="tiff_lzw" saves a great deal |
| BMP | No | No | Uncompressed; almost never the right answer |
⚠️
Saving an RGBA image as JPEG raises
OSError: cannot write mode RGBA as JPEG. Composite the transparency onto a solid background first, or save to PNG or WebP, which can store an alpha channel.FAQ
Why does my code say "seek of closed file"?
Because
Image.open was lazy and the file closed before the pixels were read. Call im.load() (or im.copy()) inside the with block and use the loaded image afterwards.Is quality=100 the best choice?
Not for the web. Above roughly 90 the file grows quickly for almost no visible gain, and re-encoding an already-compressed JPEG at 100 amplifies existing artefacts. 80-88 is the usual range for photographs.
Related
Resizing, cropping and rotating
Last refreshed 2026-09-18.