Repository: python/mypy

Issue:
`issubclass()` doesn't narrow down types of type variables
Example:
```py3
from typing import *

TypeT = TypeVar("TypeT", bound=type)

class Base:
    field: str = "Hey"

class C1:
    def method(self, other: type) -> str:
        if issubclass(other, Base):
            # reveal_type(other) == Type[Base]
            return other.field
        return "Hi"

class C2(Generic[TypeT]):
    def method(self, other: TypeT) -> str:
        if issubclass(other, Base):
            # reveal_type(other) == TypeT`1
            return other.field  # mypy error: "TypeT" has no attribute "field"
        return "Hi"
```

Actual behavior as of 0.740 included in the comments above. Expected behavior would be to not raise an error on the penultimate line.


Discussion:
Looks like an issue in `checker.py` (the logic around `issubclass()` is pretty ad-hoc there). One needs to add an `elif` for type variables in `find_isinstance_check()` there.

This might potentially need also an adjustment in `meet.py` either in `narrow_declared_type()` or in `is_overlapping_types()` or in `meet_types()`.
@ilevkivskyi I'd like to take this
> I'd like to take this

OK, go ahead.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -3733,30 +3733,7 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM
                     return {}, {}
                 expr = node.args[0]
                 if literal(expr) == LITERAL_TYPE:
-                    vartype = get_proper_type(type_map[expr])
-                    type = get_isinstance_type(node.args[1], type_map)
-                    if isinstance(vartype, UnionType):
-                        union_list = []
-                        for t in get_proper_types(vartype.items):
-                            if isinstance(t, TypeType):
-                                union_list.append(t.item)
-                            else:
-                                # This is an error that should be reported earlier
-                                # if we reach here, we refuse to do any type inference.
-                                return {}, {}
-                        vartype = UnionType(union_list)
-                    elif isinstance(vartype, TypeType):
-                        vartype = vartype.item
-                    elif (isinstance(vartype, Instance) and
-                            vartype.type.fullname == 'builtins.type'):
-                        vartype = self.named_type('builtins.object')
-                    else:
-                        # Any other object whose type we don't know precisely
-                        # for example, Any or a custom metaclass.
-                        return {}, {}  # unknown type
-                    yes_map, no_map = conditional_type_map(expr, vartype, type)
-                    yes_map, no_map = map(convert_to_typetype, (yes_map, no_map))
-                    return yes_map, no_map
+                    return self.infer_issubclass_maps(node, expr, type_map)
             elif refers_to_fullname(node.callee, 'builtins.callable'):
                 if len(node.args) != 1:  # the error will be reported elsewhere
                     return {}, {}
@@ -4367,6 +4344,39 @@ def push_type_map(self, type_map: 'TypeMap') -> None:
             for expr, type in type_map.items():
                 self.binder.put(expr, type)
 
+    def infer_issubclass_maps(self, node: CallExpr,
+                              expr: Expression,
+                              type_map: Dict[Expression, Type]
+                              ) -> Tuple[TypeMap, TypeMap]:
+        """Infer type restrictions for an expression in issubclass call."""
+        vartype = type_map[expr]
+        type = get_isinstance_type(node.args[1], type_map)
+        if isinstance(vartype, TypeVarType):
+            vartype = vartype.upper_bound
+        vartype = get_proper_type(vartype)
+        if isinstance(vartype, UnionType):
+            union_list = []
+            for t in get_proper_types(vartype.items):
+                if isinstance(t, TypeType):
+                    union_list.append(t.item)
+                else:
+                    # This is an error that should be reported earlier
+                    # if we reach here, we refuse to do any type inference.
+                    return {}, {}
+            vartype = UnionType(union_list)
+        elif isinstance(vartype, TypeType):
+            vartype = vartype.item
+        elif (isinstance(vartype, Instance) and
+                vartype.type.fullname == 'builtins.type'):
+            vartype = self.named_type('builtins.object')
+        else:
+            # Any other object whose type we don't know precisely
+            # for example, Any or a custom metaclass.
+            return {}, {}  # unknown type
+        yes_map, no_map = conditional_type_map(expr, vartype, type)
+        yes_map, no_map = map(convert_to_typetype, (yes_map, no_map))
+        return yes_map, no_map
+
 
 def conditional_type_map(expr: Expression,
                          current_type: Optional[Type],


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -6512,3 +6512,36 @@ def access_after_declaration(self) -> None:
 
     reveal_type(x)  # N: Revealed type is 'builtins.int'
     x = x + 1
+
+[case testIsSubClassNarrowDownTypesOfTypeVariables]
+from typing import Type, TypeVar, Generic
+
+class Base:
+    field: int = 42
+
+TypeT = TypeVar("TypeT", bound=type)
+
+TypeT1 = TypeVar("TypeT1", bound=Type[Base])
+
+class C1:
+    def method(self, other: type) -> int:
+        if issubclass(other, Base):
+            reveal_type(other)  # N: Revealed type is 'Type[__main__.Base]'
+            return other.field
+        return 0
+
+class C2(Generic[TypeT]):
+    def method(self, other: TypeT) -> int:
+        if issubclass(other, Base):
+            reveal_type(other)  # N: Revealed type is 'Type[__main__.Base]'
+            return other.field
+        return 0
+
+class C3(Generic[TypeT1]):
+    def method(self, other: TypeT1) -> int:
+        if issubclass(other, Base):
+            reveal_type(other)  # N: Revealed type is 'TypeT1`1'
+            return other.field
+        return 0
+
+[builtins fixtures/isinstancelist.pyi]