Repository: python/mypy

Issue:
Concatenation of tuples
Concatenation of fixed-length tuples should be supported by the type checker. 

Arbitrary tuples could be concatenated. `Tuple[int, str] + Tuple[bool]` results in `Tuple[int, str, bool]`.

Concatenating two homogeneous tuples (`Tuple[T, ...]`) already works.



Discussion:
Hi @JukkaL, any update on this feature ? Currently, concatenating two fixed-lengths tuples results in a `Unsupported operand types for +` error and a messed up type as demonstrated by the example below:

```
a = (1,) # Tuple[int]
b = ('bob',) # Tuple[str]
c = a + b # Tuple[int, str]
reveal_type(a)
reveal_type(b)
reveal_type(c)
```

```
$ mypy a.py 
a.py:5: error: Unsupported operand types for + ("Tuple[int]" and "Tuple[str]")
a.py:13: error: Revealed type is 'Tuple[builtins.int]'
a.py:14: error: Revealed type is 'Tuple[builtins.str]'
a.py:15: error: Revealed type is 'builtins.tuple[builtins.int*]'
```
Unfortunately nobody is working on this and this is not in our near-term roadmap. Adding a priority label so that contributors may be more likely to pick this up.
Currently the concatenation of two tuples is accepted by mypy but the result has type `Tuple[Any, ...]`.
This still give an error when mixing fixed length and homogeneous, see https://github.com/python/mypy/issues/6337
I would like to have support for that feature too. The following code:

``` python
from typing import *

def d2(x: int, y: int) -> Tuple[int, int]:
    return (x, y)

def d3(x: int, y: int, z: int) -> Tuple[int, int, int]:
    return d2(x, y) + (z,)
```

results in the mypy error: `Incompatible return value type (got "Tuple[Any, ...]", expected "Tuple[int, int, int]")`

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1886,6 +1886,11 @@ def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Ty
                 column=typ.column,
             ))
 
+    def concat_tuples(self, left: TupleType, right: TupleType) -> TupleType:
+        """Concatenate two fixed length tuples."""
+        return TupleType(items=left.items + right.items,
+                         fallback=self.named_type('builtins.tuple'))
+
     def visit_int_expr(self, e: IntExpr) -> Type:
         """Type check an integer literal (trivial)."""
         return self.infer_literal_expr_type(e.value, 'builtins.int')
@@ -1945,6 +1950,16 @@ def visit_op_expr(self, e: OpExpr) -> Type:
                     return self.strfrm_checker.check_str_interpolation(e.left, e.right)
         left_type = self.accept(e.left)
 
+        proper_left_type = get_proper_type(left_type)
+        if isinstance(proper_left_type, TupleType) and e.op == '+':
+            left_add_method = proper_left_type.partial_fallback.type.get('__add__')
+            if left_add_method and left_add_method.fullname == 'builtins.tuple.__add__':
+                proper_right_type = get_proper_type(self.accept(e.right))
+                if isinstance(proper_right_type, TupleType):
+                    right_radd_method = proper_right_type.partial_fallback.type.get('__radd__')
+                    if right_radd_method is None:
+                        return self.concat_tuples(proper_left_type, proper_right_type)
+
         if e.op in nodes.op_methods:
             method = self.get_operator_method(e.op)
             result, method_type = self.check_op(method, left_type, e.right, e,


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
@@ -738,7 +738,7 @@ class C: pass
 
 a = None # type: A
 
-(a, a) + a  # E: Unsupported left operand type for + ("Tuple[A, A]")
+(a, a) + a  # E: Unsupported operand types for + ("Tuple[A, A]" and "A")
 a + (a, a)  # E: Unsupported operand types for + ("A" and "Tuple[A, A]")
 f((a, a))   # E: Argument 1 to "f" has incompatible type "Tuple[A, A]"; expected "A"
 (a, a).foo  # E: "Tuple[A, A]" has no attribute "foo"
@@ -1233,3 +1233,11 @@ reveal_type(tup[2])  # N: Revealed type is 'Union[Any, builtins.int*]' \
 reveal_type(tup[:])  # N: Revealed type is 'Union[Tuple[builtins.int, builtins.str], builtins.list[builtins.int*]]'
 
 [builtins fixtures/tuple.pyi]
+
+[case testFixedLengthTupleConcatenation]
+a = (1, "foo", 3)
+b = ("bar", 7)
+
+reveal_type(a + b)  # N: Revealed type is 'Tuple[builtins.int, builtins.str, builtins.int, builtins.str, builtins.int]'
+
+[builtins fixtures/tuple.pyi]
\ No newline at end of file