Photos from phones carry EXIF data: often the GPS position where they were taken, the phone model and the time. If your Python app stores or republishes photos that users upload, removing it protects them. Removing EXIF data in Python looks like a one-liner: open the image with Pillow and save it again. With current Pillow that does remove the metadata. It also turns phone photos sideways, throws away the colour profile, and cuts JPEG quality to 75. We tested the common answers on a phone-style photo and wrote down what each one really does.
What happens when you remove EXIF with Pillow?
The metadata goes, but three other things go wrong. Our test photo was a JPEG built the way phones store pictures: the pixels saved lying on their side, with an EXIF Orientation tag telling viewers to rotate them. It carries a Display P3 colour profile, GPS, the camera model, a date and an XMP author. We also used a Stable Diffusion PNG with its prompt, a WebP with GPS, and a JPEG with C2PA Content Credentials. All tests used Pillow 12.
| Method | Metadata gone? | Upright? | Colour profile | JPEG quality |
|---|---|---|---|---|
Image.open(f).save(out) |
Yes | No, sideways | Dropped | Re-saved at 75 |
The popular getdata() / putdata() answer |
Yes | No, sideways | Dropped | Re-saved at 75 |
| The function below | Yes | Yes | Kept | Re-saved at 95 |
| ExifTool, lossless (below) | Yes | Yes | Kept | Untouched |
Why is my photo sideways after removing EXIF in Python?
Sideways photos. Phones don't rotate the pixels; they save them as the sensor saw them and add an Orientation tag. Remove the EXIF and the tag goes with it, so a portrait photo shows up lying on its side. You have to turn the pixels upright before you drop the metadata. ImageOps.exif_transpose() does that.
Colour shift. Phone photos usually carry a Display P3 profile. Without it, viewers assume sRGB, and the colours come out duller. The profile isn't personal information, so keep it.
Quality. When you don't pass quality, Pillow saves JPEGs at 75. On a 12-megapixel test image the file shrank by more than half, and the loss was clearly measurable. Every save re-compresses, so each round costs a little more.
The getdata() answer copies every pixel into a Python list and builds a new image from it. It was about 7 times slower than a plain save on a 12MP photo, uses a lot of memory, and has the same three problems.
How to remove EXIF data from an image in Python
from PIL import Image, ImageOps
def remove_metadata(src, dst):
with Image.open(src) as im:
fmt = im.format # JPEG, PNG, WEBP...
icc = im.info.get("icc_profile") # keep the colour profile
im = ImageOps.exif_transpose(im) # turn the pixels upright first
options = {"format": fmt}
if icc:
options["icc_profile"] = icc
if fmt == "JPEG":
options["quality"] = 95
im.save(dst, **options)
It removed EXIF, GPS, XMP, the Stable Diffusion prompt and the C2PA data from all four test files. The photo came out upright and kept its colour profile. src and dst can be paths or file objects, so it works on uploads too:
from io import BytesIO
cleaned = BytesIO()
remove_metadata(request.files["photo"], cleaned) # Flask; in Django use request.FILES["photo"]
cleaned.seek(0)
Two limits. It re-encodes JPEGs, so quality 95 is close to the original but not identical. And for animated GIFs or WebPs it keeps only the first frame unless you add save_all=True.
How to remove metadata in Python without losing quality
If the pixels must stay exactly the same, don't decode the image at all. ExifTool edits the file directly:
exiftool -all= --icc_profile:all -tagsfromfile @ -Orientation -overwrite_original photo.jpg
-all=removes everything--icc_profile:allkeeps the colour profile-tagsfromfile @ -Orientationwrites the Orientation tag back, so the photo stays upright
From Python:
import subprocess
subprocess.run(["exiftool", "-all=", "--icc_profile:all", "-tagsfromfile", "@",
"-Orientation", "-overwrite_original", path], check=True)
On our test photo, the pixels were identical before and after, and the orientation and colour profile were kept. Add -r and a folder path to clean a whole folder. ExifTool needs to be installed on the machine; it's free.
A popular package, piexif, has a remove() function, but it only removes the EXIF block. XMP, IPTC and C2PA stay.
What about HEIC photos, videos and PDFs?
- HEIC (iPhone's default format): Pillow can't open it without the
pillow-heifplugin. - Video, PDF and RAW files: they need different tools, and the usual ones have traps of their own.
- Proof: neither method tells you whether anything was left. That matters most when files leave your system automatically.
For all of those, our metadata removal API takes images, video, PDF and RAW in one call. It re-reads every output and reports what was removed:
import os, requests
r = requests.post("https://api.aimetadatacleaner.com/v1/clean",
headers={"Authorization": "Bearer " + os.environ["AMC_API_KEY"]},
files={"file": open("photo.jpg", "rb")})
report = r.json()
print(report["status"], report["removed"])
open("photo-clean.jpg", "wb").write(requests.get(report["download_url"]).content)
Add mode=strip to keep image pixels byte-for-byte identical. The API is part of the Business plan. For a one-off, our image cleaner runs free in your browser.
Frequently asked questions
How do I remove EXIF data from an image in Python?
Open it with Pillow, keep the colour profile, apply ImageOps.exif_transpose(), then save without passing exif. Set quality=95 for JPEGs. The full function is above. For a lossless result, call ExifTool from Python instead.
Does Pillow keep EXIF data when saving?
Not by default, in current versions: it writes EXIF only if you pass exif= when saving. It also drops the colour profile unless you pass icc_profile=, and saves JPEGs at quality 75.
Does removing EXIF in Python remove GPS?
Yes. GPS is stored in the EXIF block, and both methods above remove it. Check the result with exiftool -gps:all photo.jpg: an empty result means it's gone.
How do I check that EXIF data was removed?
Run exiftool photo.jpg and look for GPS, Make, Model and dates, or in Python check that Image.open(path).getexif() is empty. Our API reports what it removed from each file and re-reads the result.
