hugovk: This'll need release notes. 

Shall we add a simple regression test that the new one is an instance of the old one? 
diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst
index 374a8d2dd..386a26757 100644
--- a/docs/releasenotes/7.0.0.rst
+++ b/docs/releasenotes/7.0.0.rst
@@ -63,11 +63,11 @@ TODO
 API Additions
 =============
 
-TODO
-^^^^
-
-TODO
+Custom unidentified image error
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
+Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
+identified. For backwards compatibility, this will inherit from ``IOError``.
 
 Other Changes
 =============
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index 80235bba6..54c88712c 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -23,7 +23,7 @@
 # purposes only.
 
 
-from . import ImageFile, ImagePalette
+from . import ImageFile, ImagePalette, UnidentifiedImageError
 from ._binary import i8, i16be as i16, i32be as i32
 
 ##
@@ -82,4 +82,4 @@ def open(fp, mode="r"):
     try:
         return GdImageFile(fp)
     except SyntaxError:
-        raise IOError("cannot identify this image file")
+        raise UnidentifiedImageError("cannot identify this image file")
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index f50b319d2..b04b4af93 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -38,7 +38,7 @@ import warnings
 # VERSION was removed in Pillow 6.0.0.
 # PILLOW_VERSION was removed in Pillow 7.0.0.
 # Use __version__ instead.
-from . import ImageMode, TiffTags, __version__, _plugins
+from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
 from ._binary import i8, i32le
 from ._util import deferred_error, isPath, isStringType, py3
 
@@ -2794,7 +2794,9 @@ def open(fp, mode="r"):
         fp.close()
     for message in accept_warnings:
         warnings.warn(message)
-    raise IOError("cannot identify image file %r" % (filename if filename else fp))
+    raise UnidentifiedImageError(
+        "cannot identify image file %r" % (filename if filename else fp)
+    )
 
 
 #
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index fd02b40d9..e7f26488d 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -70,3 +70,7 @@ _plugins = [
     "XpmImagePlugin",
     "XVThumbImagePlugin",
 ]
+
+
+class UnidentifiedImageError(IOError):
+    pass

Add a specific exception if image not recognized?
Hello,

if the file provided as parameter to `Image.open()` is not recognized, an `IOError` is raised. It's nice but the exception is not really precise and, in python3.3+, it equals to `OSError` so it can be surprising depending on the used traceback.

The raised exception could be specific like `UnknownImageError`. It should inherit from `IOError` for backward compatibility.

The main modification would be in Image.py, line 2730:
```python
    for message in accept_warnings:
        warnings.warn(message)
-    raise IOError("cannot identify image file %r"
+    raise UnknownImageError("cannot identify image file %r"
                 % (filename if filename else fp))
```

What do you think about it? I will send a PR if you agree with the idea.

diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py
index 9208dcbff..0b7543a31 100644
--- a/Tests/test_file_gd.py
+++ b/Tests/test_file_gd.py
@@ -1,4 +1,4 @@
-from PIL import GdImageFile
+from PIL import GdImageFile, UnidentifiedImageError
 
 from .helper import PillowTestCase
 
@@ -17,4 +17,4 @@ class TestFileGd(PillowTestCase):
     def test_invalid_file(self):
         invalid_file = "Tests/images/flower.jpg"
 
-        self.assertRaises(IOError, GdImageFile.open, invalid_file)
+        self.assertRaises(UnidentifiedImageError, GdImageFile.open, invalid_file)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index b494b17dc..90bf9d1cf 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -2,7 +2,7 @@ import os
 import shutil
 import tempfile
 
-from PIL import Image
+from PIL import Image, UnidentifiedImageError
 from PIL._util import py3
 
 from .helper import PillowTestCase, hopper, is_win32, unittest
@@ -48,6 +48,9 @@ class TestImage(PillowTestCase):
                 Image.new(mode, (1, 1))
             self.assertEqual(str(e.exception), "unrecognized image mode")
 
+    def test_exception_inheritance(self):
+        self.assertTrue(issubclass(UnidentifiedImageError, IOError))
+
     def test_sanity(self):
 
         im = Image.new("L", (100, 100))
@@ -88,7 +91,7 @@ class TestImage(PillowTestCase):
             import StringIO
 
             im = StringIO.StringIO("")
-        self.assertRaises(IOError, Image.open, im)
+        self.assertRaises(UnidentifiedImageError, Image.open, im)
 
     def test_bad_mode(self):
         self.assertRaises(ValueError, Image.open, "filename", "bad mode")

pip install -e .[dev]
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc libjpeg-dev zlib1g-dev libtiff5-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev libharfbuzz-dev libfribidi-dev libxcb1-dev
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
black==25.1.0
blessed==1.20.0
build==1.2.2.post1
certifi==2025.1.31
charset-normalizer==3.4.1
check-manifest==0.50
click==8.1.8
coverage==7.8.0
coveralls==4.0.1
docopt==0.6.2
docutils==0.21.2
exceptiongroup==1.2.2
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
jarn.viewdoc==2.7
Jinja2==3.1.6
MarkupSafe==3.0.2
mypy-extensions==1.0.0
olefile==0.47
packaging==24.2
pathspec==0.12.1
-e git+https://github.com/python-pillow/Pillow.git@1c9275c2498bb84bd3f659de4ddd18f17b214067#egg=Pillow
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pyproject_hooks==1.2.0
pyroma==4.2
pytest==8.3.5
pytest-cov==6.0.0
requests==2.32.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
tomli==2.2.1
trove-classifiers==2025.3.19.19
typing_extensions==4.13.0
urllib3==2.3.0
wcwidth==0.2.13
zipp==3.21.0

name: Pillow
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - black==25.1.0
      - blessed==1.20.0
      - build==1.2.2.post1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - check-manifest==0.50
      - click==8.1.8
      - coverage==7.8.0
      - coveralls==4.0.1
      - docopt==0.6.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jarn-viewdoc==2.7
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mypy-extensions==1.0.0
      - olefile==0.47
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pyproject-hooks==1.2.0
      - pyroma==4.2
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - requests==2.32.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - trove-classifiers==2025.3.19.19
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - wcwidth==0.2.13
      - zipp==3.21.0
prefix: /opt/conda/envs/Pillow

Tests/test_file_gd.py::TestFileGd::test_bad_mode Tests/test_file_gd.py::TestFileGd::test_invalid_file Tests/test_file_gd.py::TestFileGd::test_sanity Tests/test_image.py::TestImage::test__new Tests/test_image.py::TestImage::test_alpha_composite Tests/test_image.py::TestImage::test_alpha_inplace Tests/test_image.py::TestImage::test_bad_mode Tests/test_image.py::TestImage::test_check_size Tests/test_image.py::TestImage::test_comparison_with_other_type Tests/test_image.py::TestImage::test_dump Tests/test_image.py::TestImage::test_effect_mandelbrot Tests/test_image.py::TestImage::test_effect_mandelbrot_bad_arguments Tests/test_image.py::TestImage::test_effect_noise Tests/test_image.py::TestImage::test_effect_spread Tests/test_image.py::TestImage::test_exception_inheritance Tests/test_image.py::TestImage::test_expand_x Tests/test_image.py::TestImage::test_expand_xy Tests/test_image.py::TestImage::test_fp_name Tests/test_image.py::TestImage::test_fromstring Tests/test_image.py::TestImage::test_getbands Tests/test_image.py::TestImage::test_getbbox Tests/test_image.py::TestImage::test_getchannel Tests/test_image.py::TestImage::test_getchannel_wrong_params Tests/test_image.py::TestImage::test_image_modes_fail Tests/test_image.py::TestImage::test_image_modes_success Tests/test_image.py::TestImage::test_internals Tests/test_image.py::TestImage::test_invalid_image Tests/test_image.py::TestImage::test_linear_gradient Tests/test_image.py::TestImage::test_linear_gradient_wrong_mode Tests/test_image.py::TestImage::test_load_on_nonexclusive_multiframe Tests/test_image.py::TestImage::test_ne Tests/test_image.py::TestImage::test_no_resource_warning_on_save Tests/test_image.py::TestImage::test_offset_not_implemented Tests/test_image.py::TestImage::test_overrun Tests/test_image.py::TestImage::test_p_from_rgb_rgba Tests/test_image.py::TestImage::test_pathlib Tests/test_image.py::TestImage::test_radial_gradient Tests/test_image.py::TestImage::test_radial_gradient_wrong_mode Tests/test_image.py::TestImage::test_readonly_save Tests/test_image.py::TestImage::test_register_extensions Tests/test_image.py::TestImage::test_registered_extensions Tests/test_image.py::TestImage::test_registered_extensions_uninitialized Tests/test_image.py::TestImage::test_remap_palette Tests/test_image.py::TestImage::test_sanity Tests/test_image.py::TestImage::test_storage_neg Tests/test_image.py::TestImage::test_tempfile Tests/test_image.py::TestImage::test_unknown_extension Tests/test_image.py::TestImage::test_width_height Tests/test_image.py::TestRegistry::test_encode_registry Tests/test_image.py::TestRegistry::test_encode_registry_fail
src/PIL/GdImageFile.py docs/releasenotes/7.0.0.rst src/PIL/Image.py src/PIL/__init__.py
src/PIL/GdImageFile.py docs/releasenotes/7.0.0.rst src/PIL/Image.py src/PIL/__init__.py