Metadata carries capture dates, GPS location and colour profiles: you need it for photo apps and must strip it for privacy.
Pillow exposes metadata in two places.
im.info is a dict of format-specific extras found while opening: dpi, icc_profile (bytes), transparency, duration for animations, PNG text chunks, and the raw exif bytes.
im.getexif() returns an `Image.Exif` object, a mapping from numeric tag ids to values. ExifTags.Base gives readable names: exif[ExifTags.Base.Model], ExifTags.Base.Orientation (0x0112), DateTime. Camera details and GPS are in sub-directories (IFDs): exif.get_ifd(ExifTags.IFD.Exif) for exposure settings and exif.get_ifd(ExifTags.IFD.GPSInfo) for location.
To write, modify the Exif object and pass it to save(..., exif=exif). Supported for JPEG, PNG, WebP, TIFF and others.
Pillow mostly drops metadata when saving unless you pass it: a re-saved JPEG loses EXIF and its ICC profile by default. That is good for privacy (removes GPS coordinates) but changes colours for wide-gamut photos if the ICC profile is lost. A common policy for user uploads:
- apply
ImageOps.exif_transposefirst, - keep
icc_profile=im.info.get("icc_profile")so colours stay right, - omit
exif=to strip location and device details.
PNG text chunks can be written with PngImagePlugin.PngInfo. XMP data appears in im.info or via im.getxmp() when defusedxml is installed.
Example
from PIL import ExifTags, Image
im = Image.new("RGB", (64, 48), "green")
exif = Image.Exif()
exif[ExifTags.Base.Make] = "ACME"
exif[ExifTags.Base.Model] = "Cam 1"
exif[ExifTags.Base.DateTime] = "2026:05:01 12:00:00"
im.save("tagged.jpg", exif=exif, dpi=(300, 300))
src = Image.open("tagged.jpg")
tags = src.getexif()
print(tags[ExifTags.Base.Model], tags.get(ExifTags.Base.Make)) # Cam 1 ACME
names = {ExifTags.TAGS.get(k, k): v for k, v in tags.items()}
print(sorted(names)) # ['DateTime', 'Make', 'Model']
print(src.info.get("dpi")) # (300, 300)
src.save("clean.jpg") # no exif= : metadata dropped
print(len(Image.open("clean.jpg").getexif())) # 0
keep = {"icc_profile": src.info.get("icc_profile")} # keep colours, drop EXIF
src.save("web.jpg", **{k: v for k, v in keep.items() if v})Remember
im.infoholds format extras (dpi, icc_profile, duration...)im.getexif()maps tag ids to values; use ExifTags.Base for names- Pass
exif=andicc_profile=to save to keep them - Re-saving without them strips metadata, useful for privacy
Watch out for
- Stripping the ICC profile along with EXIF, so wide-gamut photos look washed out.