Saving: formats, quality and options

Pillow · Essentials · 10 min

File size and visual quality depend almost entirely on the format and options you choose at save time.

im.save(fp, format=None, **options) writes the image. Without format, Pillow uses the file extension; when saving to a file object such as io.BytesIO, you must pass format=.

Choosing a format:

  • JPEG: lossy, small, for photos. No alpha. quality= from 1 to 95 (default 75; above 95 just grows files), optimize=True, progressive=True. subsampling=0 keeps full colour resolution for sharp text or graphics.
  • PNG: lossless, supports alpha and palettes; best for screenshots, diagrams and masks. optimize=True and compress_level=0..9 trade CPU for size, never quality.
  • WebP: lossy or lossless (lossless=True), with alpha, usually smaller than JPEG and PNG.
  • GIF: palette-based, mainly for simple animation.
  • TIFF: archival and scientific, many compressions; AVIF is supported in recent Pillow versions.

Options are format specific; unknown keywords are mostly ignored, so check the format docs. Saving a re-opened JPEG again loses quality every time (generation loss), so keep a lossless master if you edit repeatedly.

Image.registered_extensions() maps extensions to formats, and features.check("webp") tells you whether optional codecs are available in your build.

For web uploads and APIs, saving to io.BytesIO() gives you bytes without touching disk.

Example

import io
from PIL import Image

im = Image.effect_noise((400, 300), 60).convert("RGB")   # noisy test photo

sizes = {}
for q in (30, 75, 95):
    buf = io.BytesIO()
    im.save(buf, format="JPEG", quality=q, optimize=True)
    sizes[q] = buf.tell()
print(sizes[30] < sizes[75] < sizes[95])   # True: quality costs bytes

png = io.BytesIO()
im.save(png, format="PNG", optimize=True)
webp = io.BytesIO()
im.save(webp, format="WEBP", quality=80)
print(png.tell() > webp.tell())            # True for photo-like content

im.save("photo.jpg", quality=85, progressive=True)   # format from extension
print(Image.open("photo.jpg").format)       # JPEG
print(Image.registered_extensions()[".webp"])        # WEBP

Remember

  • The extension picks the format; pass format= for file objects
  • JPEG for photos (quality 1-95), PNG for lossless/alpha, WebP for both
  • Options are format-specific: quality, optimize, progressive, lossless
  • Re-saving JPEGs compounds quality loss

Watch out for

  • Saving to an io.BytesIO without format=, which raises because there is no extension to infer from.

Check yourself

  1. You need a screenshot with crisp text and a transparent background. Which format fits best?
  2. What happens when you save with `quality=100` instead of 95 for JPEG?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/handbook/image-file-formats.html