diff --git a/bugbear.py b/bugbear.py
index e45d059..eb96788 100644
--- a/bugbear.py
+++ b/bugbear.py
@@ -174,6 +174,7 @@ class BugBearVisitor(ast.NodeVisitor):
                 #    (MyError, MyError)  # duplicate names
                 #    (MyError, BaseException)  # everything derives from the Base
                 #    (Exception, TypeError)  # builtins where one subclasses another
+                #    (IOError, OSError)  # IOError is an alias of OSError since Python3.3
                 # but note that other cases are impractical to hande from the AST.
                 # We expect this is mostly useful for users who do not have the
                 # builtin exception hierarchy memorised, and include a 'shadowed'
@@ -181,6 +182,14 @@ class BugBearVisitor(ast.NodeVisitor):
                 good = sorted(set(names), key=names.index)
                 if "BaseException" in good:
                     good = ["BaseException"]
+                # Find and remove aliases exceptions and only leave the primary alone
+                primaries = filter(
+                    lambda primary: primary in good, B014.exception_aliases.keys()
+                )
+                for primary in primaries:
+                    aliases = B014.exception_aliases[primary]
+                    good = list(filter(lambda e: e not in aliases, good))
+
                 for name, other in itertools.permutations(tuple(good), 2):
                     if issubclass(
                         getattr(builtins, name, type), getattr(builtins, other, ())
@@ -639,6 +648,16 @@ B014 = Error(
         "Write `except {2}{1}:`, which catches exactly the same exceptions."
     )
 )
+B014.exception_aliases = {
+    "OSError": {
+        "IOError",
+        "EnvironmentError",
+        "WindowsError",
+        "mmap.error",
+        "socket.error",
+        "select.error",
+    }
+}
 
 # Those could be false positives but it's more dangerous to let them slip
 # through if they're not.

B014 recommends `except ():`
In the following code:

```
    try:
        with open(fn) as f:
            return f.read().split('\n')
    except (OSError, IOError):
        pass
```

The new B014 warning shows this:

> B014 Redundant exception types in `except (OSError, IOError):`.  Write `except ():`, which catches exactly the same exceptions.

Doesn't seem to make sense to me. Is this a bug :bear:?
diff --git a/tests/b014.py b/tests/b014.py
index 4c26794..a3e64e6 100644
--- a/tests/b014.py
+++ b/tests/b014.py
@@ -1,6 +1,6 @@
 """
 Should emit:
-B014 - on lines 10, 16, 27, 41, and 48
+B014 - on lines 10, 16, 27, 41, 48, and 55
 """
 
 import re
@@ -48,3 +48,14 @@ try:
 except (re.error, re.error):
     # Duplicate exception types as attributes
     pass
+
+
+try:
+    pass
+except (IOError, EnvironmentError, OSError):
+    # Detect if a primary exception and any its aliases are present.
+    #
+    # Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
+    # socket.error and select.error are aliases of OSError. See PEP 3151 for
+    # more info.
+    pass
diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py
index bc4c61a..d6161af 100644
--- a/tests/test_bugbear.py
+++ b/tests/test_bugbear.py
@@ -178,6 +178,7 @@ class BugbearTestCase(unittest.TestCase):
             B014(27, 0, vars=("MyError, MyError", "", "MyError")),
             B014(41, 0, vars=("MyError, BaseException", " as e", "BaseException")),
             B014(48, 0, vars=("re.error, re.error", "", "re.error")),
+            B014(55, 0, vars=("IOError, EnvironmentError, OSError", "", "OSError"),),
         )
         self.assertEqual(errors, expected)
 

pip install -e .[dev]
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
black==25.1.0
click==8.1.8
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
flake8==7.2.0
-e git+https://github.com/PyCQA/flake8-bugbear.git@c18db4fa8bdc15f0194541055dd4cf46dbaf18f1#egg=flake8_bugbear
hypothesis==6.130.5
hypothesmith==0.3.3
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
lark==1.2.2
libcst==1.7.0
mccabe==0.7.0
mypy-extensions==1.0.0
packaging @ file:///croot/packaging_1734472117206/work
pathspec==0.12.1
platformdirs==4.3.7
pluggy @ file:///croot/pluggy_1733169602837/work
pycodestyle==2.13.0
pyflakes==3.3.2
pytest @ file:///croot/pytest_1738938843180/work
PyYAML==6.0.2
sortedcontainers==2.4.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions==4.13.0

name: flake8-bugbear
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=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
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - black==25.1.0
      - click==8.1.8
      - coverage==7.8.0
      - flake8==7.2.0
      - hypothesis==6.130.5
      - hypothesmith==0.3.3
      - lark==1.2.2
      - libcst==1.7.0
      - mccabe==0.7.0
      - mypy-extensions==1.0.0
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pyyaml==6.0.2
      - sortedcontainers==2.4.0
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/flake8-bugbear

tests/test_bugbear.py::BugbearTestCase::test_b014
tests/test_bugbear.py::BugbearTestCase::test_b001 tests/test_bugbear.py::BugbearTestCase::test_b002 tests/test_bugbear.py::BugbearTestCase::test_b003 tests/test_bugbear.py::BugbearTestCase::test_b004 tests/test_bugbear.py::BugbearTestCase::test_b005 tests/test_bugbear.py::BugbearTestCase::test_b006_b008 tests/test_bugbear.py::BugbearTestCase::test_b007 tests/test_bugbear.py::BugbearTestCase::test_b009_b010 tests/test_bugbear.py::BugbearTestCase::test_b011 tests/test_bugbear.py::BugbearTestCase::test_b012 tests/test_bugbear.py::BugbearTestCase::test_b013 tests/test_bugbear.py::BugbearTestCase::test_b301_b302_b305 tests/test_bugbear.py::BugbearTestCase::test_b303_b304 tests/test_bugbear.py::BugbearTestCase::test_b306 tests/test_bugbear.py::BugbearTestCase::test_b901 tests/test_bugbear.py::BugbearTestCase::test_b902 tests/test_bugbear.py::BugbearTestCase::test_b903 tests/test_bugbear.py::BugbearTestCase::test_b950 tests/test_bugbear.py::BugbearTestCase::test_selfclean_bugbear tests/test_bugbear.py::BugbearTestCase::test_selfclean_test_bugbear tests/test_bugbear.py::TestFuzz::test_does_not_crash_on_any_valid_code tests/test_bugbear.py::TestFuzz::test_does_not_crash_on_site_code
bugbear.py
bugbear.py