Short answer

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

read_exif.py
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}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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)

exif_details.py
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}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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:

date_taken.py
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
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
2026-01-31 18:41:57
None

GPS coordinates in decimal degrees

gps.py
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}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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:

orientation.py
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))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
Orientation tag: 6
after exif_transpose: (800, 1200) tag: None

Remove EXIF (privacy) or just the GPS

strip_exif.py
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'}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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

write_exif.py
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])
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
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():

old_vs_new.py
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")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
True - the old private method still exists on JPEGs
Exif - use the public getexif() instead

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.