Repository: python/mypy

Issue:
Types should be promoted by issubclass
I expected types to be promoted to sub-types within an `issubclass` block, the same as instances within an `isinstance` block, but type-checking the following code results in the error `Type[Vehicle] has no attribute "wheels"`:

```python
from typing import Type

class Vehicle(object):
    name = ''

class Car(Vehicle):
    wheels = 4

def test_isinstance(x):
    # type: (Vehicle) -> None
    print(x.name)
    if isinstance(x, Car):
        print(x.wheels)

def test_issubclass(x):
    # type: (Type[Vehicle]) -> None
    print(x.name)
    if issubclass(x, Car):
        print(x.wheels)
```

Is this be possible or am I missing something?  I'm testing against the master branch.





Discussion:
This would be an interesting feature. Also I think the implementation would be straightforward, since ``isinstance`` is already understood by mypy.
Yeah, this would be a reasonable thing to support, though you can currently work around the limitation pretty easily by using `cast(Type[Subclass], x)` after the `issubclass` check. Example:

```py
...

def test_issubclass(x):
    # type: (Type[Vehicle]) -> None
    print(x.name)
    if issubclass(x, Car):
        print(cast(Type[car], x).wheels)
```

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2675,6 +2675,21 @@ def or_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap:
     return result
 
 
+def convert_to_typetype(type_map: TypeMap) -> TypeMap:
+    converted_type_map = {}  # type: TypeMap
+    if type_map is None:
+        return None
+    for expr, typ in type_map.items():
+        if isinstance(typ, UnionType):
+            converted_type_map[expr] = UnionType([TypeType(t) for t in typ.items])
+        elif isinstance(typ, Instance):
+            converted_type_map[expr] = TypeType(typ)
+        else:
+            # unknown type; error was likely reported earlier
+            return {}
+    return converted_type_map
+
+
 def find_isinstance_check(node: Expression,
                           type_map: Dict[Expression, Type],
                           ) -> Tuple[TypeMap, TypeMap]:
@@ -2700,8 +2715,32 @@ def find_isinstance_check(node: Expression,
             expr = node.args[0]
             if expr.literal == LITERAL_TYPE:
                 vartype = type_map[expr]
-                types = get_isinstance_type(node.args[1], type_map)
-                return conditional_type_map(expr, vartype, types)
+                type = get_isinstance_type(node.args[1], type_map)
+                return conditional_type_map(expr, vartype, type)
+        elif refers_to_fullname(node.callee, 'builtins.issubclass'):
+            expr = node.args[0]
+            if expr.literal == LITERAL_TYPE:
+                vartype = type_map[expr]
+                type = get_isinstance_type(node.args[1], type_map)
+                if isinstance(vartype, UnionType):
+                    union_list = []
+                    for t in 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
+                else:
+                    # any other object whose type we don't know precisely
+                    # for example, Any or Instance of type type
+                    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
         elif refers_to_fullname(node.callee, 'builtins.callable'):
             expr = node.args[0]
             if expr.literal == LITERAL_TYPE:
@@ -2793,18 +2832,18 @@ def flatten_types(t: Type) -> List[Type]:
 def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]:
     all_types = flatten_types(type_map[expr])
     types = []  # type: List[TypeRange]
-    for type in all_types:
-        if isinstance(type, FunctionLike) and type.is_type_obj():
+    for typ in all_types:
+        if isinstance(typ, FunctionLike) and typ.is_type_obj():
             # Type variables may be present -- erase them, which is the best
             # we can do (outside disallowing them here).
-            type = erase_typevars(type.items()[0].ret_type)
-            types.append(TypeRange(type, is_upper_bound=False))
-        elif isinstance(type, TypeType):
+            typ = erase_typevars(typ.items()[0].ret_type)
+            types.append(TypeRange(typ, is_upper_bound=False))
+        elif isinstance(typ, TypeType):
             # Type[A] means "any type that is a subtype of A" rather than "precisely type A"
             # we indicate this by setting is_upper_bound flag
-            types.append(TypeRange(type.item, is_upper_bound=True))
-        elif isinstance(type, Instance) and type.type.fullname() == 'builtins.type':
-            object_type = Instance(type.type.mro[-1], [])
+            types.append(TypeRange(typ.item, is_upper_bound=True))
+        elif isinstance(typ, Instance) and typ.type.fullname() == 'builtins.type':
+            object_type = Instance(typ.type.mro[-1], [])
             types.append(TypeRange(object_type, is_upper_bound=True))
         else:  # we didn't see an actual type, but rather a variable whose value is unknown to us
             return None
@@ -2955,17 +2994,17 @@ def is_more_precise_signature(t: CallableType, s: CallableType) -> bool:
     return is_more_precise(t.ret_type, s.ret_type)
 
 
-def infer_operator_assignment_method(type: Type, operator: str) -> Tuple[bool, str]:
+def infer_operator_assignment_method(typ: Type, operator: str) -> Tuple[bool, str]:
     """Determine if operator assignment on given value type is in-place, and the method name.
 
     For example, if operator is '+', return (True, '__iadd__') or (False, '__add__')
     depending on which method is supported by the type.
     """
     method = nodes.op_methods[operator]
-    if isinstance(type, Instance):
+    if isinstance(typ, Instance):
         if operator in nodes.ops_with_inplace_method:
             inplace_method = '__i' + method[2:]
-            if type.type.has_readable_member(inplace_method):
+            if typ.type.has_readable_member(inplace_method):
                 return True, inplace_method
     return False, method
 


Test patch:
diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test
--- a/test-data/unit/check-isinstance.test
+++ b/test-data/unit/check-isinstance.test
@@ -1423,6 +1423,213 @@ def f(x: Union[int, A], a: Type[A]) -> None:
 [builtins fixtures/isinstancelist.pyi]
 
 
+[case testIssubclassUnreachable]
+from typing import Type, Sequence, Union
+x: Type[str]
+if issubclass(x, int):
+    reveal_type(x)  # unreachable block
+
+
+class X: pass
+class Y(X): pass
+class Z(X): pass
+
+a: Union[Type[Y], Type[Z]]
+if issubclass(a, X):
+    reveal_type(a)  # E: Revealed type is 'Union[Type[__main__.Y], Type[__main__.Z]]'
+else:
+    reveal_type(a)  # unreachable block
+
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIssubclasDestructuringUnions]
+from typing import Union, List, Tuple, Dict, Type
+def f(x: Union[Type[int], Type[str], Type[List]]) -> None:
+    if issubclass(x, (str, (int,))):
+        reveal_type(x)  # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]'
+        reveal_type(x())  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+        x()[1]  # E: Value of type "Union[int, str]" is not indexable
+    else:
+        reveal_type(x)  # E: Revealed type is 'Type[builtins.list]'
+        reveal_type(x())  # E: Revealed type is 'builtins.list[<uninhabited>]'
+        x()[1]
+    reveal_type(x)  # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]'
+    reveal_type(x())  # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<uninhabited>]]'
+    if issubclass(x, (str, (list,))):
+        reveal_type(x)  # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
+        reveal_type(x())  # E: Revealed type is 'Union[builtins.str, builtins.list[<uninhabited>]]'
+        x()[1]
+    reveal_type(x)  # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
+    reveal_type(x())  # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<uninhabited>]]'
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIssubclass]
+from typing import Type, ClassVar
+
+class Goblin:
+    level: int
+
+class GoblinAmbusher(Goblin):
+    job: ClassVar[str] = 'Ranger'
+
+def test_issubclass(cls: Type[Goblin]) -> None:
+    if issubclass(cls, GoblinAmbusher):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+        cls.level
+        cls.job
+        ga = cls()
+        ga.level = 15
+        ga.job
+        ga.job = "Warrior"  # E: Cannot assign to class variable "job" via instance
+    else:
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.Goblin]'
+        cls.level
+        cls.job  # E: Type[Goblin] has no attribute "job"
+        g = cls()
+        g.level = 15
+        g.job  # E: "Goblin" has no attribute "job"
+
+
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIssubclassDeepHierarchy]
+from typing import Type, ClassVar
+
+class Mob:
+    pass
+
+class Goblin(Mob):
+    level: int
+
+class GoblinAmbusher(Goblin):
+    job: ClassVar[str] = 'Ranger'
+
+def test_issubclass(cls: Type[Mob]) -> None:
+    if issubclass(cls, Goblin):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.Goblin]'
+        cls.level
+        cls.job  # E: Type[Goblin] has no attribute "job"
+        g = cls()
+        g.level = 15
+        g.job  # E: "Goblin" has no attribute "job"
+        if issubclass(cls, GoblinAmbusher):
+            reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+            cls.level
+            cls.job
+            g = cls()
+            g.level = 15
+            g.job
+            g.job = 'Warrior' # E: Cannot assign to class variable "job" via instance
+    else:
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.Mob]'
+        cls.job  # E: Type[Mob] has no attribute "job"
+        cls.level  # E: Type[Mob] has no attribute "level"
+        m = cls()
+        m.level = 15  # E: "Mob" has no attribute "level"
+        m.job  # E: "Mob" has no attribute "job"
+        if issubclass(cls, GoblinAmbusher):
+            reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+            cls.job
+            cls.level
+            ga = cls()
+            ga.level = 15
+            ga.job
+            ga.job = 'Warrior' # E: Cannot assign to class variable "job" via instance
+
+    if issubclass(cls, GoblinAmbusher):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+        cls.level
+        cls.job
+        ga = cls()
+        ga.level = 15
+        ga.job
+        ga.job = "Warrior"  # E: Cannot assign to class variable "job" via instance
+
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIssubclassTuple]
+from typing import Type, ClassVar
+
+class Mob:
+    pass
+
+class Goblin(Mob):
+    level: int
+
+class GoblinAmbusher(Goblin):
+    job: ClassVar[str] = 'Ranger'
+
+class GoblinDigger(Goblin):
+    job: ClassVar[str] = 'Thief'
+
+def test_issubclass(cls: Type[Mob]) -> None:
+    if issubclass(cls, (Goblin, GoblinAmbusher)):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.Goblin]'
+        cls.level
+        cls.job  # E: Type[Goblin] has no attribute "job"
+        g = cls()
+        g.level = 15
+        g.job  # E: "Goblin" has no attribute "job"
+        if issubclass(cls, GoblinAmbusher):
+            cls.level
+            reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+            cls.job
+            ga = cls()
+            ga.level = 15
+            ga.job
+            ga.job = "Warrior"  # E: Cannot assign to class variable "job" via instance
+    else:
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.Mob]'
+        cls.job  # E: Type[Mob] has no attribute "job"
+        cls.level  # E: Type[Mob] has no attribute "level"
+        m = cls()
+        m.level = 15  # E: "Mob" has no attribute "level"
+        m.job  # E: "Mob" has no attribute "job"
+        if issubclass(cls, GoblinAmbusher):
+            reveal_type(cls)  # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
+            cls.job
+            cls.level
+            ga = cls()
+            ga.level = 15
+            ga.job
+            ga.job = "Warrior"  # E: Cannot assign to class variable "job" via instance
+
+    if issubclass(cls, (GoblinDigger, GoblinAmbusher)):
+        reveal_type(cls)  # E: Revealed type is 'Union[Type[__main__.GoblinDigger], Type[__main__.GoblinAmbusher]]'
+        cls.level
+        cls.job
+        g = cls()
+        g.level = 15
+        g.job
+        g.job = "Warrior" # E: Cannot assign to class variable "job" via instance
+
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIssubclassBuiltins]
+from typing import List, Type
+
+class MyList(List): pass
+class MyIntList(List[int]): pass
+
+def f(cls: Type[object]) -> None:
+    if issubclass(cls, MyList):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.MyList]'
+        cls()[0]
+    else:
+        reveal_type(cls)  # E: Revealed type is 'Type[builtins.object]'
+        cls()[0]  # E: Value of type "object" is not indexable
+
+    if issubclass(cls, MyIntList):
+        reveal_type(cls)  # E: Revealed type is 'Type[__main__.MyIntList]'
+        cls()[0] + 1
+
+[builtins fixtures/isinstancelist.pyi]
+
 [case testIsinstanceTypeArgs]
 from typing import Iterable, TypeVar
 x = 1