exif = Image.open(path).getexif() gives the top-level tags (camera make and model, orientation). Camera
settings and the date taken are in exif.get_ifd(IFD.Exif), and GPS is in exif.get_ifd(IFD.GPSInfo).
Use PIL.ExifTags.TAGS to turn tag numbers into names.
The examples read camera.jpg, our real photo with EXIF written by the setup code (camera, lens, settings and the
GPS position of Launch Pad 39B), because NASA's published files carry no camera EXIF.
Read the main tags
from PIL import Image
from PIL.ExifTags import TAGS
img = Image.open("camera.jpg")
exif = img.getexif()
for tag_id, value in exif.items():
print(f"{TAGS.get(tag_id, tag_id):<16} {value}")
GPSInfo 300
ExifOffset 154
Make Canon
Model Canon EOS R5
Software Pillow example
Orientation 1
DateTime 2026:01:31 18:42:10
Notice that the date the photo was taken and the exposure settings are missing. In the main directory, the
ExifOffset and GPSInfo entries are just pointers to sub-directories.
Camera settings and the date taken (the Exif sub-directory)
from PIL import Image
from PIL.ExifTags import IFD, TAGS
exif = Image.open("camera.jpg").getexif()
details = exif.get_ifd(IFD.Exif) # camera settings live in this sub-directory
for tag_id, value in details.items():
print(f"{TAGS.get(tag_id, tag_id):<18} {value}")
DateTimeOriginal 2026:01:31 18:41:57
FocalLength 70.0
LensModel RF24-105mm F4 L IS USM
ISOSpeedRatings 400
ExposureTime 0.004
FNumber 8.0
Date taken as a datetime
DateTimeOriginal is when the shutter fired. The top-level DateTime is when the file was last changed,
often by editing software, so use it only as a fallback:
from datetime import datetime
from PIL import Image
from PIL.ExifTags import Base, IFD
def date_taken(path):
"""When the photo was taken, or None. Prefers DateTimeOriginal over the file's DateTime."""
exif = Image.open(path).getexif()
raw = exif.get_ifd(IFD.Exif).get(Base.DateTimeOriginal) or exif.get(Base.DateTime)
return datetime.strptime(raw, "%Y:%m:%d %H:%M:%S") if raw else None
print(date_taken("camera.jpg"))
print(date_taken("photo.png")) # no EXIF
2026-01-31 18:41:57
None
GPS coordinates in decimal degrees
from PIL import Image
from PIL.ExifTags import GPS, IFD
def to_degrees(dms, ref):
d, m, s = (float(x) for x in dms)
value = d + m / 60 + s / 3600
return -value if ref in ("S", "W") else value
gps = Image.open("camera.jpg").getexif().get_ifd(IFD.GPSInfo)
lat = to_degrees(gps[GPS.GPSLatitude], gps[GPS.GPSLatitudeRef])
lon = to_degrees(gps[GPS.GPSLongitude], gps[GPS.GPSLongitudeRef])
print("raw:", gps[GPS.GPSLatitude], gps[GPS.GPSLatitudeRef])
print(f"decimal: {lat:.5f}, {lon:.5f}")
print(f"map: https://www.openstreetmap.org/?mlat={lat:.5f}&mlon={lon:.5f}")
raw: (28.0, 37.0, 37.8) N
decimal: 28.62717, -80.62100
map: https://www.openstreetmap.org/?mlat=28.62717&mlon=-80.62100
South latitudes and west longitudes are negative in decimal form, which is what map services expect.
Orientation
Tag 274 (0x0112) tells viewers how to rotate the stored pixels. Apply it once with exif_transpose(),
which also resets the tag so nothing rotates the image twice:
from PIL import Image, ImageOps
img = Image.open("phone.jpg")
print("Orientation tag:", img.getexif().get(0x0112))
upright = ImageOps.exif_transpose(img)
print("after exif_transpose:", upright.size, "tag:", upright.getexif().get(0x0112))
Orientation tag: 6
after exif_transpose: (800, 1200) tag: None
Remove EXIF (privacy) or just the GPS
from PIL import Image
from PIL.ExifTags import IFD
img = Image.open("camera.jpg")
exif = img.getexif()
exif.pop(IFD.GPSInfo, None) # remove location only
img.save("no_gps.jpg", exif=exif, quality=90)
img.save("no_exif.jpg", quality=90) # no exif= argument: all EXIF dropped
for name in ["camera.jpg", "no_gps.jpg", "no_exif.jpg"]:
e = Image.open(name).getexif()
print(f"{name:<12} tags={len(e):<3} GPS={'yes' if e.get_ifd(IFD.GPSInfo) else 'no'}")
camera.jpg tags=7 GPS=yes
no_gps.jpg tags=6 GPS=no
no_exif.jpg tags=0 GPS=no
Pillow writes no EXIF unless you pass exif=, so re-saving strips everything. That is the safest default for user
uploads. Remember that this also drops the orientation tag: call exif_transpose() first, or portrait photos will
come out sideways.
Write or change tags
from PIL import Image
from PIL.ExifTags import Base
img = Image.open("camera.jpg")
exif = img.getexif()
exif[Base.Artist] = "Jane Doe"
exif[Base.Copyright] = "CC BY 4.0"
img.save("tagged.jpg", exif=exif, quality=90)
e = Image.open("tagged.jpg").getexif()
print(e[Base.Artist], "|", e[Base.Copyright])
Jane Doe | CC BY 4.0
_getexif() vs getexif()
Many older answers use img._getexif(). It is private, only exists for some formats, and returns a plain dict
mixing sub-directory data in. Use getexif():
from PIL import Image
img = Image.open("camera.jpg")
print(hasattr(img, "_getexif"), "- the old private method still exists on JPEGs")
print(type(img.getexif()).__name__, "- use the public getexif() instead")
True - the old private method still exists on JPEGs
Exif - use the public getexif() instead
Related
The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.