Repository: python/mypy

Issue:
(🎁) Intersections with `@final` types should trigger unreachable errors
```py
from typing import final

@final
class A: ...
class B: ...

a: A
assert isinstance(a, B)
reveal_type(a)  # note: Revealed type is "__main__.<subclass of "A" and "B">"
print("hi") # no unreachable error
```

- Relates to #12163


Discussion:
"Intersections with `@final` types should trigger unreachable errors"… (unless `A` already inherits from `B` of course :smile:)

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -5232,6 +5232,15 @@ def _make_fake_typeinfo_and_full_name(
         pretty_names_list = pretty_seq(
             format_type_distinctly(*base_classes, options=self.options, bare=True), "and"
         )
+
+        new_errors = []
+        for base in base_classes:
+            if base.type.is_final:
+                new_errors.append((pretty_names_list, f'"{base.type.name}" is final'))
+        if new_errors:
+            errors.extend(new_errors)
+            return None
+
         try:
             info, full_name = _make_fake_typeinfo_and_full_name(base_classes, curr_module)
             with self.msg.filter_errors() as local_errors:
@@ -5244,10 +5253,10 @@ def _make_fake_typeinfo_and_full_name(
                     self.check_multiple_inheritance(info)
             info.is_intersection = True
         except MroError:
-            errors.append((pretty_names_list, "inconsistent method resolution order"))
+            errors.append((pretty_names_list, "would have inconsistent method resolution order"))
             return None
         if local_errors.has_new_errors():
-            errors.append((pretty_names_list, "incompatible method signatures"))
+            errors.append((pretty_names_list, "would have incompatible method signatures"))
             return None
 
         curr_module.names[full_name] = SymbolTableNode(GDEF, info)
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -2044,7 +2044,7 @@ def redundant_expr(self, description: str, truthiness: bool, context: Context) -
     def impossible_intersection(
         self, formatted_base_class_list: str, reason: str, context: Context
     ) -> None:
-        template = "Subclass of {} cannot exist: would have {}"
+        template = "Subclass of {} cannot exist: {}"
         self.fail(
             template.format(formatted_base_class_list, reason), context, code=codes.UNREACHABLE
         )


Test patch:
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
--- a/test-data/unit/check-narrowing.test
+++ b/test-data/unit/check-narrowing.test
@@ -1020,6 +1020,105 @@ else:
     reveal_type(true_or_false)  # N: Revealed type is "Literal[False]"
 [builtins fixtures/primitives.pyi]
 
+
+[case testNarrowingIsInstanceFinalSubclass]
+# flags: --warn-unreachable
+
+from typing import final
+
+class N: ...
+@final
+class F1: ...
+@final
+class F2: ...
+
+n: N
+f1: F1
+
+if isinstance(f1, F1):
+    reveal_type(f1)  # N: Revealed type is "__main__.F1"
+else:
+    reveal_type(f1)  # E: Statement is unreachable
+
+if isinstance(n, F1):  # E: Subclass of "N" and "F1" cannot exist: "F1" is final
+    reveal_type(n)  # E: Statement is unreachable
+else:
+    reveal_type(n)  # N: Revealed type is "__main__.N"
+
+if isinstance(f1, N):  # E: Subclass of "F1" and "N" cannot exist: "F1" is final
+    reveal_type(f1)  # E: Statement is unreachable
+else:
+    reveal_type(f1)  # N: Revealed type is "__main__.F1"
+
+if isinstance(f1, F2):  # E: Subclass of "F1" and "F2" cannot exist: "F1" is final \
+                        # E: Subclass of "F1" and "F2" cannot exist: "F2" is final
+    reveal_type(f1)  # E: Statement is unreachable
+else:
+    reveal_type(f1)  # N: Revealed type is "__main__.F1"
+[builtins fixtures/isinstance.pyi]
+
+
+[case testNarrowingIsInstanceFinalSubclassWithUnions]
+# flags: --warn-unreachable
+
+from typing import final, Union
+
+class N: ...
+@final
+class F1: ...
+@final
+class F2: ...
+
+n_f1: Union[N, F1]
+n_f2: Union[N, F2]
+f1_f2: Union[F1, F2]
+
+if isinstance(n_f1, F1):
+    reveal_type(n_f1)  # N: Revealed type is "__main__.F1"
+else:
+    reveal_type(n_f1)  # N: Revealed type is "__main__.N"
+
+if isinstance(n_f2, F1):  # E: Subclass of "N" and "F1" cannot exist: "F1" is final \
+                          # E: Subclass of "F2" and "F1" cannot exist: "F2" is final \
+                          # E: Subclass of "F2" and "F1" cannot exist: "F1" is final
+    reveal_type(n_f2)  # E: Statement is unreachable
+else:
+    reveal_type(n_f2)  # N: Revealed type is "Union[__main__.N, __main__.F2]"
+
+if isinstance(f1_f2, F1):
+    reveal_type(f1_f2)  # N: Revealed type is "__main__.F1"
+else:
+    reveal_type(f1_f2)  # N: Revealed type is "__main__.F2"
+[builtins fixtures/isinstance.pyi]
+
+
+[case testNarrowingIsSubclassFinalSubclassWithTypeVar]
+# flags: --warn-unreachable
+
+from typing import final, Type, TypeVar
+
+@final
+class A: ...
+@final
+class B: ...
+
+T = TypeVar("T", A, B)
+
+def f(cls: Type[T]) -> T:
+    if issubclass(cls, A):
+        reveal_type(cls)  # N: Revealed type is "Type[__main__.A]"
+        x: bool
+        if x:
+            return A()
+        else:
+            return B()  # E: Incompatible return value type (got "B", expected "A")
+    assert False
+
+reveal_type(f(A))  # N: Revealed type is "__main__.A"
+reveal_type(f(B))  # N: Revealed type is "__main__.B"
+[builtins fixtures/isinstance.pyi]
+
+
 [case testNarrowingLiteralIdentityCheck]
 from typing import Union
 from typing_extensions import Literal