Repository: python/mypy

Issue:
Incorrect inference with isinstance()
I think there are a few problems with `isinstance` inference.

1) This code type checks with `mypy --strcit --python-version 3.6`, but it should not:
```
from typing import *
def f(x: Union[int, str], typ: type) -> None:
    if isinstance(x, (typ, int)):
        print(x + 1)
```
The semantic analyzer concludes that within the body of `if isinstance()`, the type of `x` must be `int`. It's not, however. For example, in this case `f('a', str)`, the `if` branch will be followed even though `x` is not an `int` (and this will cause runtime `TypeError` without any mypy warnings).

The reason this happens is that `type` annotation is represented as an `Instance` rather than as a `FunctionLike` inside  `checker.py:get_isinstance_type()`. As a result, it's skipped entirely in the loop, and only `int` is added to `types`, so sem analyzer thinks `x` must absolutely be `int`.

One solution is to make `type` represented with an instance of `CallableType` (why isn't it btw?!). Another solution is to simply add a bit of logic to the loop to properly handle this case.

2) This code:
```
def f(x: Union[int, str, List, Tuple]) -> None:
    if isinstance(x, (str, (list, tuple))):
        print(x[1])
```
results in `error: Argument 2 to "isinstance" has incompatible type "Tuple[str, Tuple[List[_T], Tuple[_T_co, ...]]]"; expected "Union[type, Tuple[type, ...]]"`. 

This is because there's this weird thing in python that allows to put *nested* tuples as the second argument to `isinstance`, and `mypy` (along with most other people) doesn't know about it. I don't know if it's worth fixing, but it's super easy (just flatten the second argument if it's a tuple).

If my understanding is correct, I'll make a PR.


Discussion:
Yes, the way type objects are represented right now is kind of messy. Until we clean up the implementation, mypy should support all the possible representations of type objects in all contexts.

The second case would be a reasonable thing to fix if the fix is simple enough, though clearly it's not a high priority thing. Could you actually create a separate issue for the second case, since it seems like an independent issue?
Actually, I was wrong: the 1) issue has nothing to do with whether `type` is a `CallableType` or an `Instance` (and I understand now why it's not quite a good fit for `CallableType`). The problem is that from the perspective of  type checker, the second argument to `isinstance` can be either a literal type (when the source code directly refers to a class name), or a variable that contains the type (when the source code uses a function argument or something else). In the second case, the type checker cannot really infer anything since the value inside the variable won't be known statically. So I don't think there's any need to fix the representation of `type`, just a small logic change to the function will fix the problem.

And yes, I can separate them out.
Ah yeah, I didn't read the first example correctly. Yes, if one the types is a `type` instance, it can match any type, but it could also not match any type. For example:

```
from typing import *
def f(x: Union[int, str], typ: type) -> None:
    if isinstance(x, (typ, int)):
        reveal_type(x)  # should be Union[int, str]
    else:
        reveal_type(x)  # should be str
```
```
I'm moving item 2) into a separate issue as you suggested.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2452,10 +2452,19 @@ def push_type_map(self, type_map: Optional[Dict[Expression, Type]]) -> None:
 
 TypeMap = Optional[Dict[Expression, Type]]
 
+# An object that represents either a precise type or a type with an upper bound;
+# it is important for correct type inference with isinstance.
+TypeRange = NamedTuple(
+    'TypeRange',
+    [
+        ('item', Type),
+        ('is_upper_bound', bool),  # False => precise type
+    ])
+
 
 def conditional_type_map(expr: Expression,
                          current_type: Optional[Type],
-                         proposed_type: Optional[Type],
+                         proposed_type_ranges: Optional[List[TypeRange]],
                          ) -> Tuple[TypeMap, TypeMap]:
     """Takes in an expression, the current type of the expression, and a
     proposed type of that expression.
@@ -2463,17 +2472,26 @@ def conditional_type_map(expr: Expression,
     Returns a 2-tuple: The first element is a map from the expression to
     the proposed type, if the expression can be the proposed type.  The
     second element is a map from the expression to the type it would hold
-    if it was not the proposed type, if any."""
-    if proposed_type:
+    if it was not the proposed type, if any. None means bot, {} means top"""
+    if proposed_type_ranges:
+        if len(proposed_type_ranges) == 1:
+            proposed_type = proposed_type_ranges[0].item  # Union with a single type breaks tests
+        else:
+            proposed_type = UnionType([type_range.item for type_range in proposed_type_ranges])
         if current_type:
-            if is_proper_subtype(current_type, proposed_type):
-                # Expression is always of type proposed_type
+            if (not any(type_range.is_upper_bound for type_range in proposed_type_ranges)
+               and is_proper_subtype(current_type, proposed_type)):
+                # Expression is always of one of the types in proposed_type_ranges
                 return {}, None
             elif not is_overlapping_types(current_type, proposed_type):
-                # Expression is never of type proposed_type
+                # Expression is never of any type in proposed_type_ranges
                 return None, {}
             else:
-                remaining_type = restrict_subtype_away(current_type, proposed_type)
+                # we can only restrict when the type is precise, not bounded
+                proposed_precise_type = UnionType([type_range.item
+                                          for type_range in proposed_type_ranges
+                                          if not type_range.is_upper_bound])
+                remaining_type = restrict_subtype_away(current_type, proposed_precise_type)
                 return {expr: proposed_type}, {expr: remaining_type}
         else:
             return {expr: proposed_type}, {}
@@ -2644,8 +2662,8 @@ def find_isinstance_check(node: Expression,
             expr = node.args[0]
             if expr.literal == LITERAL_TYPE:
                 vartype = type_map[expr]
-                type = get_isinstance_type(node.args[1], type_map)
-                return conditional_type_map(expr, vartype, type)
+                types = get_isinstance_type(node.args[1], type_map)
+                return conditional_type_map(expr, vartype, types)
         elif refers_to_fullname(node.callee, 'builtins.callable'):
             expr = node.args[0]
             if expr.literal == LITERAL_TYPE:
@@ -2663,7 +2681,8 @@ def find_isinstance_check(node: Expression,
                     # two elements in node.operands, and at least one of them
                     # should represent a None.
                     vartype = type_map[expr]
-                    if_vars, else_vars = conditional_type_map(expr, vartype, NoneTyp())
+                    none_typ = [TypeRange(NoneTyp(), is_upper_bound=False)]
+                    if_vars, else_vars = conditional_type_map(expr, vartype, none_typ)
                     break
 
             if is_not:
@@ -2725,26 +2744,30 @@ def flatten(t: Expression) -> List[Expression]:
         return [t]
 
 
-def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> Type:
+def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]:
     all_types = [type_map[e] for e in flatten(expr)]
-
-    types = []  # type: List[Type]
-
+    types = []  # type: List[TypeRange]
     for type in all_types:
-        if isinstance(type, FunctionLike):
-            if type.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(type)
-
-    if len(types) == 0:
+        if isinstance(type, FunctionLike) and type.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):
+            # 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(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
+    if not types:
+        # this can happen if someone has empty tuple as 2nd argument to isinstance
+        # strictly speaking, we should return UninhabitedType but for simplicity we will simply
+        # refuse to do any type inference for now
         return None
-    elif len(types) == 1:
-        return types[0]
-    else:
-        return UnionType(types)
+    return types
 
 
 def expand_func(defn: FuncItem, map: Dict[TypeVarId, Type]) -> FuncItem:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -2679,8 +2679,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
             # This branch handles the case foo.bar where foo is a module.
             # In this case base.node is the module's MypyFile and we look up
             # bar in its namespace.  This must be done for all types of bar.
-            file = base.node
-            assert isinstance(file, (MypyFile, type(None)))
+            file = cast(Optional[MypyFile], base.node)  # can't use isinstance due to issue #2999
             n = file.names.get(expr.name, None) if file is not None else None
             if n:
                 n = self.normalize_type_alias(n, expr)


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
@@ -1360,3 +1360,55 @@ def f(x: object) -> None:
     reveal_type(b)  # E: Revealed type is '__main__.A'
 [builtins fixtures/isinstance.pyi]
 [out]
+
+
+[case testIsInstanceWithUnknownType]
+from typing import Union
+def f(x: Union[int, str], typ: type) -> None:
+    if isinstance(x, (typ, int)):
+        x + 1  # E: Unsupported operand types for + (likely involving Union)
+        reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+    else:
+        reveal_type(x)  # E: Revealed type is 'builtins.str'
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIsInstanceWithBoundedType]
+from typing import Union, Type
+
+class A: pass
+def f(x: Union[int, A], a: Type[A]) -> None:
+    if isinstance(x, (a, int)):
+        reveal_type(x)  # E: Revealed type is 'Union[builtins.int, __main__.A]'
+    else:
+        reveal_type(x)  # E: Revealed type is '__main__.A'
+
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIsInstanceWithEmtpy2ndArg]
+from typing import Union
+def f(x: Union[int, str]) -> None:
+    if isinstance(x, ()):
+        reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+    else:
+        reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+[builtins fixtures/isinstancelist.pyi]
+
+
+[case testIsInstanceWithTypeObject]
+from typing import Union, Type
+
+class A: pass
+
+def f(x: Union[int, A], a: Type[A]) -> None:
+    if isinstance(x, a):
+        reveal_type(x)  # E: Revealed type is '__main__.A'
+    elif isinstance(x, int):
+        reveal_type(x)  # E: Revealed type is 'builtins.int'
+    else:
+        reveal_type(x)  # E: Revealed type is '__main__.A'
+    reveal_type(x)  # E: Revealed type is 'Union[builtins.int, __main__.A]'
+
+[builtins fixtures/isinstancelist.pyi]
+