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 usable
  • im.size is a (width, height) tuple; crop and paste take coordinates in that same order.
  • im.mode tells you what the pixel data means — the most useful single property when a save fails.
  • im.info holds 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_transpose is applied.
  • For multi-frame files (GIF, TIFF) use im.n_frames and im.seek(n); ImageSequence.Iterator wraps 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)
FormatAlphaLossyNotes
JPEGNoYesquality 1-95 (75 default); progressive helps the web
PNGYesNooptimize=True; excellent for screenshots, poor for photos
WebPYesBothlossless=True switches mode; method controls encode effort
GIF1-bitPalettesave_all=True for animation, loop=0 for forever
TIFFYesNo by defaultcompression="tiff_lzw" saves a great deal
BMPNoNoUncompressed; 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.

Resizing, cropping and rotating

Last refreshed 2026-09-18.