Repository: python/mypy

Issue:
isinstance checks don't work with tuple types
An invalid type is inferred for `x` after `isinstance` in this example:

```
from typing import Union, Tuple

def f(x: Union[str, Tuple[str, str]]) -> None:
    if isinstance(x, tuple):
        reveal_type(x)  # Revealed type is 'builtins.None'
```

This was originally reported by @davire at gitter.


Discussion:

Resolving patch:
diff --git a/mypy/meet.py b/mypy/meet.py
--- a/mypy/meet.py
+++ b/mypy/meet.py
@@ -227,6 +227,8 @@ def visit_instance(self, t: Instance) -> Type:
                         return NoneTyp()
         elif isinstance(self.s, TypeType):
             return meet_types(t, self.s)
+        elif isinstance(self.s, TupleType):
+            return meet_types(t, self.s)
         else:
             return self.default(self.s)
 
@@ -243,6 +245,10 @@ def visit_tuple_type(self, t: TupleType) -> Type:
                 items.append(self.meet(t.items[i], self.s.items[i]))
             # TODO: What if the fallbacks are different?
             return TupleType(items, t.fallback)
+        # meet(Tuple[t1, t2, <...>], Tuple[s, ...]) == Tuple[meet(t1, s), meet(t2, s), <...>].
+        elif (isinstance(self.s, Instance) and
+              self.s.type.fullname() == 'builtins.tuple' and self.s.args):
+            return t.copy_modified(items=[meet_types(it, self.s.args[0]) for it in t.items])
         else:
             return self.default(self.s)
 


Test patch:
diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test
--- a/test-data/unit/check-tuples.test
+++ b/test-data/unit/check-tuples.test
@@ -808,6 +808,84 @@ b = (1, 'x')
 a = (0, *b, '')
 [builtins fixtures/tuple.pyi]
 
+[case testTupleMeetTupleAny]
+# flags: --hide-error-context
+from typing import Union, Tuple
+class A: pass
+class B: pass
+
+def f(x: Union[B, Tuple[A, A]]) -> None:
+    if isinstance(x, tuple):
+        reveal_type(x) # E: Revealed type is 'Tuple[__main__.A, __main__.A]'
+    else:
+        reveal_type(x) # E: Revealed type is '__main__.B'
+
+def g(x: Union[str, Tuple[str, str]]) -> None:
+    if isinstance(x, tuple):
+        reveal_type(x) # E: Revealed type is 'Tuple[builtins.str, builtins.str]'
+    else:
+        reveal_type(x) # E: Revealed type is 'builtins.str'
+
+[builtins fixtures/tuple.pyi]
+[out]
+
+[case testTupleMeetTUpleAnyComplex]
+# flags: --hide-error-context
+from typing import Tuple, Union
+
+Pair = Tuple[int, int]
+Variant = Union[int, Pair]
+def tuplify(v: Variant) -> None:
+    reveal_type(v) # E: Revealed type is 'Union[builtins.int, Tuple[builtins.int, builtins.int]]'
+    if not isinstance(v, tuple):
+        reveal_type(v) # E: Revealed type is 'builtins.int'
+        v = (v, v)
+        reveal_type(v) # E: Revealed type is 'Tuple[builtins.int, builtins.int]'
+    reveal_type(v) # E: Revealed type is 'Tuple[builtins.int, builtins.int]'
+    reveal_type(v[0]) # E: Revealed type is 'builtins.int'
+
+Pair2 = Tuple[int, str]
+Variant2 = Union[int, Pair2]
+def tuplify2(v: Variant2) -> None:
+    if isinstance(v, tuple):
+        reveal_type(v) # E: Revealed type is 'Tuple[builtins.int, builtins.str]'
+    else:
+        reveal_type(v) # E: Revealed type is 'builtins.int'
+[builtins fixtures/tuple.pyi]
+[out]
+
+[case testTupleMeetTupleAnyAfter]
+# flags: --hide-error-context
+from typing import Tuple, Union
+
+def good(blah: Union[Tuple[int, int], int]) -> None:
+    reveal_type(blah) # E: Revealed type is 'Union[Tuple[builtins.int, builtins.int], builtins.int]'
+    if isinstance(blah, tuple):
+        reveal_type(blah) # E: Revealed type is 'Tuple[builtins.int, builtins.int]'
+    reveal_type(blah) # E: Revealed type is 'Union[Tuple[builtins.int, builtins.int], builtins.int]'
+[builtins fixtures/tuple.pyi]
+[out]
+
+[case testTupleMeetTupleVariable]
+from typing import Tuple, TypeVar, Generic, Union
+T = TypeVar('T')
+
+class A: pass
+class B1(A): pass
+class B2(A): pass
+class C: pass
+
+x = None # type: Tuple[A, ...]
+y = None # type: Tuple[Union[B1, C], Union[B2, C]]
+
+def g(x: T) -> Tuple[T, T]:
+    return (x, x)
+
+z = 1
+x, y = g(z) # E: Argument 1 to "g" has incompatible type "int"; expected "Tuple[B1, B2]"
+[builtins fixtures/tuple.pyi]
+[out]
+
 [case testTupleWithUndersizedContext]
 a = ([1], 'x')
 a = ([], 'x', 1)  # E: Incompatible types in assignment (expression has type "Tuple[List[int], str, int]", variable has type "Tuple[List[int], str]")