Repository: python/mypy

Issue:
isinstance(x, (t1, t2)) doesn't change mypy's knowledge of x's type.
For example in the following code:

```
def f(x):
  # type: (object) -> list
  if isinstance(x, (list, set)):
    return [g(i) for i in x]
```

mypy will complain that an `Iterable` is expected (it still only sees `x` as an `object`). This can be resolved with e.g. a manual `cast` of `x` to `Union[list, set]`, but this adds a slight overhead and isn't as clean.

isinstance(x, (t1, t2)) doesn't change mypy's knowledge of x's type.
For example in the following code:

```
def f(x):
  # type: (object) -> list
  if isinstance(x, (list, set)):
    return [g(i) for i in x]
```

mypy will complain that an `Iterable` is expected (it still only sees `x` as an `object`). This can be resolved with e.g. a manual `cast` of `x` to `Union[list, set]`, but this adds a slight overhead and isn't as clean.



Discussion:
Mypy doesn't understand tuples in `isinstance` yet, but this should be easy to implement.

What should the type of x be inferred to though? `Union[t1, t2]`?

If this is important I can give it a shot for the current milestone.

I just implemented this. I'll write the tests and submit a PR.

In my implementation, the type of `x` would by `Union[t1, t2]`, like @gvanrossum said.

Awesome!!

Mypy doesn't understand tuples in `isinstance` yet, but this should be easy to implement.

What should the type of x be inferred to though? `Union[t1, t2]`?

If this is important I can give it a shot for the current milestone.

I just implemented this. I'll write the tests and submit a PR.

In my implementation, the type of `x` would by `Union[t1, t2]`, like @gvanrossum said.

Awesome!!


Resolving patch:
diff --git a/lib-python/3.2/posixpath.py b/lib-python/3.2/posixpath.py
--- a/lib-python/3.2/posixpath.py
+++ b/lib-python/3.2/posixpath.py
@@ -17,7 +17,7 @@
 from genericpath import *
 
 from typing import (
-    Tuple, BinaryIO, TextIO, Pattern, AnyStr, List, Set, Any, Union
+    Tuple, BinaryIO, TextIO, Pattern, AnyStr, List, Set, Any, Union, cast
 )
 
 __all__ = ["normcase","isabs","join","splitdrive","split","splitext",
@@ -56,7 +56,7 @@ def normcase(s: AnyStr) -> AnyStr:
     if not isinstance(s, (bytes, str)):
         raise TypeError("normcase() argument must be str or bytes, "
                         "not '{}'".format(s.__class__.__name__))
-    return s
+    return cast(AnyStr, s)
 
 
 # Return whether a path is absolute.
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2412,12 +2412,29 @@ def find_isinstance_check(node: Node,
 
 def get_isinstance_type(node: Node, type_map: Dict[Node, Type]) -> Type:
     type = type_map[node]
-    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).
-            return erase_typevars(type.items()[0].ret_type)
-    return None
+
+    if isinstance(type, TupleType):
+        all_types = type.items
+    else:
+        all_types = [type]
+
+    types = []  # type: List[Type]
+
+    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:
+        return None
+    elif len(types) == 1:
+        return types[0]
+    else:
+        return UnionType(types)
 
 
 def expand_node(defn: Node, map: Dict[int, Type]) -> Node:


Test patch:
diff --git a/mypy/test/data/check-isinstance.test b/mypy/test/data/check-isinstance.test
--- a/mypy/test/data/check-isinstance.test
+++ b/mypy/test/data/check-isinstance.test
@@ -343,6 +343,30 @@ while 1:
         z = h.pet.paws + 'a' # E: Unsupported operand types for + ("int" and "str")
 [builtins fixtures/isinstancelist.py]
 
+[case testIsinstanceTuple]
+from typing import Union
+
+class A:
+    pass
+
+class B:
+    def method2(self, arg: int):
+        return 123
+
+class C:
+    def method2(self, arg: int):
+        return 456
+
+    def method3(self, arg: str):
+        return 'abc'
+
+v = A() # type: Union[A, B, C]
+
+if isinstance(v, (B, C)):
+    v.method2(123)
+    v.method3('xyz') # E: Some element of union has no attribute "method3"
+[builtins fixtures/isinstance.py]
+
 [case testMemberAssignmentChanges-skip]
 from typing import Union
 
diff --git a/mypy/test/data/fixtures/isinstance.py b/mypy/test/data/fixtures/isinstance.py
--- a/mypy/test/data/fixtures/isinstance.py
+++ b/mypy/test/data/fixtures/isinstance.py
@@ -1,4 +1,6 @@
-from typing import builtinclass
+from typing import builtinclass, Tuple, TypeVar, Generic, Union
+
+T = TypeVar('T')
 
 class object:
     def __init__(self) -> None: pass
@@ -6,10 +8,11 @@ def __init__(self) -> None: pass
 class type:
     def __init__(self, x) -> None: pass
 
-class tuple: pass
+class tuple(Generic[T]): pass
+
 class function: pass
 
-def isinstance(x: object, t: type) -> bool: pass
+def isinstance(x: object, t: Union[type, Tuple[type, ...]]) -> bool: pass
 
 class int: pass
 class float: pass