Repository: python/mypy

Issue:
assigning a `Tuple[int, ...]` to an `Iterable[int]` results in `builtins.None` or `<nothing>`
Bug with this code:

```
from typing import Iterable, Tuple
x: Iterable[int] = ()
y: Tuple[int, ...] = (1, 2, 3)
x = y
reveal_type(x)
```

Actual result (on mypy master, Python 3.6):

`tuplebug.py:9: error: Revealed type is 'builtins.None'`

Actual result with `--strict-optional`:

`tuplebug.py:9: error: Revealed type is '<nothing>'`

Expected result:

The revealed type should be `Tuple[int, ...]`.

Via git bisect, I find that this bug was introduced recently, in https://github.com/python/mypy/pull/4238 (cc @elazarg, @JukkaL ).


Discussion:
(We saw this as a regression false positive on real code, when upgrading mypy.)
Ideally we should fix this before the next mypy release (which hasn't been scheduled yet).
Thanks for the report.

I'd note that the following also fails, and is important to test since `Tuple[int, ...]` and `Tuple[int, int]` are handled quite differently inside mypy:
```python
from typing import Iterable, Tuple
x: Iterable[int] = ()
y: Tuple[int, int] = (1, 2)
x = y
reveal_type(x)  # should be Tuple[int, int], actual 'None'/'<nothing>'
```

The issue seems to be subtle.
@ilevkivskyi I think this is the same issue I saw in #5303. I am raising priority to high, since this was a regression and came up again (and I expect it is not an uncommon pattern).

Resolving patch:
diff --git a/mypy/meet.py b/mypy/meet.py
--- a/mypy/meet.py
+++ b/mypy/meet.py
@@ -7,7 +7,7 @@
     TupleType, TypedDictType, ErasedType, UnionType, PartialType, DeletedType,
     UninhabitedType, TypeType, TypeOfAny
 )
-from mypy.subtypes import is_equivalent, is_subtype, is_protocol_implementation
+from mypy.subtypes import is_equivalent, is_subtype, is_protocol_implementation, is_proper_subtype
 
 from mypy import experiments
 
@@ -118,6 +118,8 @@ class C(A, B): ...
             isinstance(s, Instance) and s.type.fullname() == 'builtins.object'):
         return True
 
+    if is_proper_subtype(t, s) or is_proper_subtype(s, t):
+        return True
     # Since we are effectively working with the erased types, we only
     # need to handle occurrences of TypeVarType at the top level.
     if isinstance(t, TypeVarType):
@@ -323,7 +325,8 @@ def visit_tuple_type(self, t: TupleType) -> Type:
             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):
+              (self.s.type.fullname() == 'builtins.tuple' or is_proper_subtype(t, self.s))
+              and self.s.args):
             return t.copy_modified(items=[meet_types(it, self.s.args[0]) for it in t.items])
         elif (isinstance(self.s, Instance) and t.fallback.type == self.s.type):
             # Uh oh, a broken named tuple type (https://github.com/python/mypy/issues/3016).


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
@@ -1065,3 +1065,42 @@ class CallableTuple(Tuple[str, int]):
         return n
 
 [builtins fixtures/tuple.pyi]
+
+[case testTupleCompatibleWithSequence]
+from typing import Sequence
+s: Sequence[str]
+s = tuple()
+reveal_type(s) # E: Revealed type is 'builtins.tuple[builtins.str]'
+
+[builtins fixtures/tuple.pyi]
+
+[case testTupleInstanceCompatibleWithIterable]
+from typing import Iterable, Tuple
+x: Iterable[int] = ()
+y: Tuple[int, ...] = (1, 2, 3)
+x = y
+reveal_type(x) # E: Revealed type is 'builtins.tuple[builtins.int]'
+
+[builtins fixtures/tuple.pyi]
+
+[case testTupleTypeCompatibleWithIterable]
+from typing import Iterable, Tuple
+x: Iterable[int] = ()
+y: Tuple[int, int] = (1, 2)
+x = y
+reveal_type(x) # E: Revealed type is 'Tuple[builtins.int, builtins.int]'
+
+[case testTupleOverlapDifferentTuples]
+from typing import Optional, Tuple
+class A: pass
+class B: pass
+
+possibles: Tuple[int, Tuple[A]]
+x: Optional[Tuple[B]]
+
+if x in possibles:
+    reveal_type(x) # E: Revealed type is 'Union[Tuple[__main__.B], None]'
+else:
+    reveal_type(x) # E: Revealed type is 'Union[Tuple[__main__.B], None]'
+
+[builtins fixtures/tuple.pyi]