Repository: PyCQA/flake8-bugbear

Issue:
New check which find duplicate except clauses
flake8-bugbear already find duplicate exeptions in the same tuple, but across different except's this is not done

**Examples**:
```python
try:
    pass
except ValueError:
    pass
except ValueError:
    pass
```
```python
try:
    pass
except (ValueError, TypeError):
    pass
except (ValueError, UnicodeError):
    pass
```

I'm happy tp contribute this check 😄 

Comments:

Patch:
diff --git a/README.rst b/README.rst
index 63e3fb5..c12f73e 100644
--- a/README.rst
+++ b/README.rst
@@ -156,6 +156,10 @@ the loop, because `late-binding closures are a classic gotcha
 
 **B024**: Abstract base class with no abstract method. Remember to use @abstractmethod, @abstractclassmethod, and/or @abstractproperty decorators.
 
+**B025**: ``try-except`` block with duplicate exceptions found.
+This check identifies exception types that are specified in multiple ``except``
+clauses. The first specification is the only one ever considered, so all others can be removed.
+
 Opinionated warnings
 ~~~~~~~~~~~~~~~~~~~~
 
diff --git a/bugbear.py b/bugbear.py
index 3db1e0d..7f95e2e 100644
--- a/bugbear.py
+++ b/bugbear.py
@@ -421,6 +421,7 @@ class BugBearVisitor(ast.NodeVisitor):
 
     def visit_Try(self, node):
         self.check_for_b012(node)
+        self.check_for_b025(node)
         self.generic_visit(node)
 
     def visit_Compare(self, node):
@@ -837,6 +838,24 @@ class BugBearVisitor(ast.NodeVisitor):
         ):
             self.errors.append(B022(node.lineno, node.col_offset))
 
+    def check_for_b025(self, node):
+        seen = []
+        for handler in node.handlers:
+            if isinstance(handler.type, (ast.Name, ast.Attribute)):
+                name = ".".join(compose_call_path(handler.type))
+                seen.append(name)
+            elif isinstance(handler.type, ast.Tuple):
+                # to avoid checking the same as B014, remove duplicates per except
+                uniques = set()
+                for entry in handler.type.elts:
+                    name = ".".join(compose_call_path(entry))
+                    uniques.add(name)
+                seen.extend(uniques)
+        # sort to have a deterministic output
+        duplicates = sorted(set(x for x in seen if seen.count(x) > 1))
+        for duplicate in duplicates:
+            self.errors.append(B025(node.lineno, node.col_offset, vars=(duplicate,)))
+
 
 def compose_call_path(node):
     if isinstance(node, ast.Attribute):
@@ -1178,6 +1197,12 @@ B024 = Error(
         " decorators."
     )
 )
+B025 = Error(
+    message=(
+        "B025 Exception `{0}` has been caught multiple times. Only the first except"
+        " will be considered and all other except catches can be safely removed."
+    )
+)
 
 # Warnings disabled by default.
 B901 = Error(


Test patch:
diff --git a/tests/b025.py b/tests/b025.py
new file mode 100644
index 0000000..085f82f
--- /dev/null
+++ b/tests/b025.py
@@ -0,0 +1,38 @@
+"""
+Should emit:
+B025 - on lines 15, 22, 31
+"""
+
+import pickle
+
+try:
+    a = 1
+except ValueError:
+    a = 2
+finally:
+    a = 3
+
+try:
+    a = 1
+except ValueError:
+    a = 2
+except ValueError:
+    a = 2
+
+try:
+    a = 1
+except pickle.PickleError:
+    a = 2
+except ValueError:
+    a = 2
+except pickle.PickleError:
+    a = 2
+
+try:
+    a = 1
+except (ValueError, TypeError):
+    a = 2
+except ValueError:
+    a = 2
+except (OSError, TypeError):
+    a = 2
diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py
index 5db013b..b09bb48 100644
--- a/tests/test_bugbear.py
+++ b/tests/test_bugbear.py
@@ -35,6 +35,7 @@ from bugbear import (
     B022,
     B023,
     B024,
+    B025,
     B901,
     B902,
     B903,
@@ -366,6 +367,20 @@ class BugbearTestCase(unittest.TestCase):
         )
         self.assertEqual(errors, expected)
 
+    def test_b025(self):
+        filename = Path(__file__).absolute().parent / "b025.py"
+        bbc = BugBearChecker(filename=str(filename))
+        errors = list(bbc.run())
+        self.assertEqual(
+            errors,
+            self.errors(
+                B025(15, 0, vars=("ValueError",)),
+                B025(22, 0, vars=("pickle.PickleError",)),
+                B025(31, 0, vars=("TypeError",)),
+                B025(31, 0, vars=("ValueError",)),
+            ),
+        )
+
     def test_b901(self):
         filename = Path(__file__).absolute().parent / "b901.py"
         bbc = BugBearChecker(filename=str(filename))