Quick fix

Look at what the file really contains: run the sniff script below, or open it in a text editor. If it starts with <!DOCTYPE html, your download failed and saved an error page. If it is 0 bytes, the download or upload never completed or was empty. If it is HEIC, install pillow-heif. If it is SVG, Pillow cannot read it.

What the error looks like

open.py
from PIL import Image

img = Image.open("downloads/cat.jpg")
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback (Pillow 12.3.0)
PIL.UnidentifiedImageError: cannot identify image file 'downloads/cat.jpg'

UnidentifiedImageError has existed since Pillow 7.0. It is a subclass of OSError, so older code that catches OSError (or IOError) still catches it.

Find out what the file really is

Every image format starts with a recognisable signature ("magic bytes"). This script checks the first bytes of each file in a folder. We ran it on a folder with the usual suspects:

sniff.py
from pathlib import Path

SIGNATURES = [
    (b"\xff\xd8\xff", "JPEG"),
    (b"\x89PNG\r\n\x1a\n", "PNG"),
    (b"GIF87a", "GIF"), (b"GIF89a", "GIF"),
    (b"BM", "BMP"),
    (b"II*\x00", "TIFF"), (b"MM\x00*", "TIFF"),
    (b"%PDF", "PDF document, not an image"),
    (b"<!doctype html", "HTML page, not an image"), (b"<html", "HTML page, not an image"),
    (b"<svg", "SVG (vector), Pillow cannot open it"), (b"<?xml", "XML, possibly SVG"),
    (b"{", "JSON, probably an API error"),
]
ISO_BRANDS = {"heic": "HEIC", "heix": "HEIC", "mif1": "HEIF", "avif": "AVIF"}


def sniff(path):
    head = Path(path).read_bytes()[:64]
    if not head:
        return "empty file (0 bytes)"
    if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
        return "WebP"
    if head[4:8] == b"ftyp":
        brand = head[8:12].decode("ascii", "replace")
        return ISO_BRANDS.get(brand, f"ISO media file, brand {brand!r}")
    text = head.lstrip().lower()
    for magic, name in SIGNATURES:
        if text.startswith(magic.lower()):
            return name
    return f"unknown, starts with {head[:12]!r}"


for path in sorted(Path("downloads").iterdir()):
    print(f"{path.name:<14} {sniff(path)}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
IMG_0042.heic  HEIC
cat.jpg        HTML page, not an image
empty.png      empty file (0 bytes)
invoice.pdf    PDF document, not an image
logo.svg       SVG (vector), Pillow cannot open it
photo.jpg      JPEG
photo.webp     WebP

Only photo.jpg and photo.webp will open in a default Pillow install. Everything else on that list raises UnidentifiedImageError, for the reasons below.

Cause 1: you saved an error page instead of an image

This is by far the most common cause in scrapers and download scripts. The server answered with a 403, 404 or a login page, and the script wrote that HTML to disk with a .jpg name:

download.py (the bug)
import requests
from PIL import Image

r = requests.get("https://example.com/cat.jpg")
open("cat.jpg", "wb").write(r.content)   # saves the 403/404 HTML page if the request failed
img = Image.open("cat.jpg")              # UnidentifiedImageError

Check the response before trusting it. Some sites also refuse requests without a User-Agent header:

download.py (the fix)
import io
import requests
from PIL import Image

r = requests.get("https://example.com/cat.jpg", timeout=30,
                 headers={"User-Agent": "my-app/1.0"})
r.raise_for_status()                                  # stop on 403, 404, 500 ...
ctype = r.headers.get("Content-Type", "")
if not ctype.startswith("image/"):
    raise ValueError(f"expected an image, got {ctype!r}: {r.text[:200]!r}")

img = Image.open(io.BytesIO(r.content))
img.load()

These two snippets need network access, so they are the only examples on this page we did not execute.

Cause 2: an empty or partial file

A 0-byte file produces exactly the same error, as does a file that was cut off before the first few bytes were written. Check the size (os.path.getsize(path)) and re-download. If the header survived but the rest did not, the error changes: open() succeeds and loading the pixels fails instead:

truncated.py
from PIL import Image

data = open("photo.jpg", "rb").read()
open("half.jpg", "wb").write(data[: len(data) // 2])   # simulate an interrupted download

img = Image.open("half.jpg")      # works: the header is intact
print(img.format, img.size)
img.load()                        # fails: the pixel data is incomplete
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
JPEG (1600, 1067)
Traceback (most recent call last):
  ...
OSError: image file is truncated (39 bytes not processed)

Only for files you trust, you can ask Pillow to decode what is there by setting ImageFile.LOAD_TRUNCATED_IMAGES = True. In our test with a half-written JPEG, the missing rows came out solid grey (128, 128, 128).

Myth: you must call seek(0) on a BytesIO first

Many answers online blame an un-rewound io.BytesIO for this error. With current Pillow that is no longer the cause: Image.open() seeks to the start of a file object itself before reading. We checked on 12.3.0:

buffer.py
import io
from PIL import Image

buf = io.BytesIO()
Image.open("photo.jpg").save(buf, format="PNG")
print("position before open:", buf.tell())

img = Image.open(buf)        # no buf.seek(0) needed
print(img.format, img.size)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
position before open: 1326212
PNG (1600, 1067)

Calling buf.seek(0) is harmless, so keep it if you support very old Pillow versions. But if a buffer or upload still fails, it is empty or does not contain image bytes:

upload.py
import io
from PIL import Image

upload = io.BytesIO(b"")     # e.g. a form field that arrived without a file
img = Image.open(upload)
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback
PIL.UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7f8d443a5170>

In Django and Flask, this usually means the form was submitted without a file, or without enctype="multipart/form-data". Check the upload's size before opening it.

Cause 4: a format Pillow does not read by default

HEIC / HEIF (iPhone photos)

Pillow does not include a HEIC decoder. Without a plugin, an iPhone photo fails like this:

heic.py
from PIL import Image

img = Image.open("downloads/IMG_0042.heic")
Tested on ✓ Linux ✓ Windows ✓ macOS
Traceback
PIL.UnidentifiedImageError: cannot identify image file 'downloads/IMG_0042.heic'

The pillow-heif package adds HEIC support and plugs into Image.open():

heic.py
from PIL import Image
from pillow_heif import register_heif_opener   # pip install pillow-heif

register_heif_opener()       # teaches Image.open() to read HEIC/HEIF

img = Image.open("downloads/IMG_0042.heic")
print(img.format, img.mode, img.size)
img.convert("RGB").save("IMG_0042.jpg", quality=90)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
HEIF RGB (1600, 1067)

AVIF, SVG and others

To see every format your installation can read, run python -m PIL --report.

Process folders without crashing

When you handle many files you do not control, skip the bad ones and log why. verify() checks a file cheaply without decoding every pixel (you need to re-open the file afterwards to actually use it):

scan_folder.py
from pathlib import Path
from PIL import Image, UnidentifiedImageError

ok, skipped = [], []
for path in sorted(Path("downloads").iterdir()):
    try:
        with Image.open(path) as img:
            img.verify()                 # checks the file without decoding all pixels
        ok.append(path.name)
    except UnidentifiedImageError:
        skipped.append((path.name, "not a recognised image"))
    except OSError as e:                 # truncated or corrupt data
        skipped.append((path.name, str(e)))

print("ok:     ", ok)
for name, why in skipped:
    print("skipped:", name, "-", why)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
ok:      ['photo.jpg', 'photo.webp']
skipped: IMG_0042.heic - not a recognised image
skipped: cat.jpg - not a recognised image
skipped: empty.png - not a recognised image
skipped: invoice.pdf - not a recognised image
skipped: logo.svg - not a recognised image

Restrict which formats users can upload

The formats argument to Image.open() does not help Pillow identify a file. It does the opposite: it limits which decoders Pillow will try. That is useful for security on upload endpoints, where you do not want to parse every format Pillow supports:

uploads.py
from PIL import Image, UnidentifiedImageError

ALLOWED = ["JPEG", "PNG", "WEBP"]


def open_upload(fp):
    """Open user uploads, refusing anything that is not JPEG, PNG or WebP."""
    img = Image.open(fp, formats=ALLOWED)
    img.load()
    return img


print(open_upload("photo.jpg").format)
try:
    open_upload("spinner.gif")
except UnidentifiedImageError as e:
    print("rejected:", e)
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
JPEG
rejected: cannot identify image file 'spinner.gif'

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.