PIL UnidentifiedImageError: cannot identify image file (tested)
PIL.UnidentifiedImageError: cannot identify image file means the bytes Pillow got do not start with any image signature it knows. In our tests that was an HTML error page saved as .jpg, an empty file, a truncated PNG, and a network response that had already been read. Print the first bytes to see which.
Test files: a real 64×64 PNG, an HTML error page saved as page.jpg, an empty empty.png, and cut.png (the first 10 bytes of the real PNG).
1. The error
from PIL import Image
Image.open("/tmp/page.jpg") # actually an HTML error page saved with a .jpg name
Output:
Traceback (most recent call last):
File "/w/b1.py", line 2, in <module>
Image.open("/tmp/page.jpg") # actually an HTML error page saved with a .jpg name
~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/site-packages/PIL/Image.py", line 3730, in open
raise UnidentifiedImageError(msg)
PIL.UnidentifiedImageError: cannot identify image file '/tmp/page.jpg'
Pillow identifies a format from the file's first bytes, not its extension. If no installed decoder recognises them, Image.open() raises UnidentifiedImageError. The most common reason is that the file is not an image at all.
2. Look at the first bytes
from PIL import Image, UnidentifiedImageError
for p in ("/tmp/page.jpg", "/tmp/empty.png", "/tmp/cut.png", "/tmp/ok.png"):
head = open(p, "rb").read(16)
try:
Image.open(p)
print(f"{p:16} OK first bytes: {head[:8]!r}")
except UnidentifiedImageError as e:
print(f"{p:16} FAILED first bytes: {head[:8]!r}")
Output:
/tmp/page.jpg FAILED first bytes: b'<!DOCTYP'
/tmp/empty.png FAILED first bytes: b''
/tmp/cut.png FAILED first bytes: b'\x89PNG\r\n\x1a\n'
/tmp/ok.png OK first bytes: b'\x89PNG\r\n\x1a\n'
The first bytes tell you what you really have: <!DOCTYP or <html is a web page (a login page, a 403 or 404 page, a redirect), b'' is an empty file from a failed download or write, and a correct signature that still fails is a truncated or corrupt file. Real signatures: PNG \x89PNG, JPEG \xff\xd8\xff, GIF GIF8, WebP RIFF....WEBP.
3. A consumed stream: pass the bytes, not the response
import io, http.server, threading, urllib.request
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (8, 8)).save(buf, "PNG")
print("BytesIO left at position", buf.tell(), "->", Image.open(buf).size) # Pillow seeks back itself
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200); self.send_header("Content-Type", "image/png"); self.end_headers()
self.wfile.write(buf.getvalue())
def log_message(self, *a): pass
srv = http.server.HTTPServer(("127.0.0.1", 8964), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
resp = urllib.request.urlopen("http://127.0.0.1:8964/x.png")
data = resp.read() # stream consumed (e.g. to check its size)
try:
Image.open(resp) # a network stream cannot seek back
except Exception as e:
print(type(e).__name__ + ":", str(e)[:70])
print("from the bytes instead ->", Image.open(io.BytesIO(data)).size)
srv.shutdown()
Output:
BytesIO left at position 69 -> (8, 8)
UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7f6c4e6384f0>
from the bytes instead -> (8, 8)
A BytesIO left at the end is fine: Pillow 13 seeks back to the start itself. A network response is not seekable, though. Once you have called .read() on it, there is nothing left, and the error names a BytesIO object because Pillow copied the (empty) remaining stream into one. Keep the bytes and open io.BytesIO(data).
4. Downloads: check Content-Type before opening
import io, urllib.request, http.server, threading
from PIL import Image, UnidentifiedImageError
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
body = b"<html>please log in</html>"
self.send_response(200); self.send_header("Content-Type", "text/html"); self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
srv = http.server.HTTPServer(("127.0.0.1", 8964), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
resp = urllib.request.urlopen("http://127.0.0.1:8964/photo.png")
data = resp.read()
ctype = resp.headers.get("Content-Type")
if not ctype.startswith("image/"):
print(f"not an image: Content-Type {ctype}, body starts {data[:30]!r}")
else:
Image.open(io.BytesIO(data))
srv.shutdown()
Output:
not an image: Content-Type text/html, body starts b'<html>please log in</html>'
A server that needs a login, blocks your user agent or rate-limits you often answers with an HTML page (sometimes with status 200). Check Content-Type and print the first bytes before handing the data to Pillow; the real problem is in that HTML.
5. Catching it
from PIL import Image, UnidentifiedImageError
print(UnidentifiedImageError.__mro__[1:3])
try:
Image.open("/tmp/page.jpg")
except OSError as e: # older code catching IOError/OSError still works
print("caught as OSError:", type(e).__name__)
Output:
(<class 'OSError'>, <class 'Exception'>)
caught as OSError: UnidentifiedImageError
UnidentifiedImageError is a subclass of OSError, so older except IOError / except OSError code still catches it. Import it with from PIL import UnidentifiedImageError to catch it precisely.
Related: PIL DecompressionBombError · BadZipFile: File is not a zip file (same root cause: an HTML page saved as a file).
Frequently asked questions
Why does Pillow say cannot identify image file?
The data is not a recognisable image: usually an HTML page saved with an image extension, an empty or truncated download, or a stream that was already read. Check the first bytes.
Do I need buf.seek(0) before Image.open(BytesIO)?
Not with Pillow 13.0.0: it seeks back itself. Non-seekable streams such as an already-read HTTP response do fail; open io.BytesIO(data) instead.
How do I catch UnidentifiedImageError?
from PIL import UnidentifiedImageError, then except UnidentifiedImageError. It is also an OSError subclass.
Written with AI assistance. Measured on 2026-10-23 with Pillow 13.0.0 and Python 3.14.7 (python:3-alpine in Docker). Each block was run verbatim and the output pasted unedited. PlainInspect is our extension; Python and Pillow are free.