Repository: python/mypy

Issue:
Better message for invalid types like (int, str)
The error message "Invalid tuple literal type" isn't very helpful if a user writes `(int, str)` instead of `Tuple[int, str]`.

Example:

```py
from typing import List

def f():
    # type: () -> (int, str)   # Invalid tuple literal type
    return (1, 'x')
```

A better message could be something like this:

```
program.py:3: error: Syntax error in type annotation
program.py:3:note: Suggestion: Use "Tuple[T1, ..., Tn]" instead of "(T1, ..., Tn)"
```

#4172 is closely related.


Discussion:
Interested. Are you suggesting literally `(T1, ..., Tn)` or substituting in the types values used, as in, `(int, str)` in the error message? I think the later makes more sense, if slightly more complex to implement.
> Are you suggesting literally (T1, ..., Tn) or substituting in the types values used, as in, (int, str) in the error message? I think the later makes more sense, if slightly more complex to implement.

Just literal `(T1, ..., Tn)` (or something similar) would be enough.
Came across this today when I accidentally had:

```python
def foo(
    arg1,  # type: int,
    arg2,  # type: str
):  # type (...) -> None
    pass
```

The lint was on the first line of the function definition, `def foo`, but the problem was an errant comma on `arg1`.
@Hnasar that is technically https://github.com/python/mypy/issues/4172, but these are, as Jukka said previously, closely related.

Resolving patch:
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -419,9 +419,11 @@ def visit_tuple_type(self, t: TupleType) -> Type:
         # Types such as (t1, t2, ...) only allowed in assignment statements. They'll
         # generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead.
         if t.implicit and not self.allow_tuple_literal:
-            self.fail('Invalid tuple literal type', t)
+            self.fail('Syntax error in type annotation', t)
             if len(t.items) == 1:
                 self.note_func('Suggestion: Is there a spurious trailing comma?', t)
+            else:
+                self.note_func('Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)', t)
             return AnyType(TypeOfAny.from_error)
         star_count = sum(1 for item in t.items if isinstance(item, StarType))
         if star_count > 1:


Test patch:
diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test
--- a/test-data/unit/check-async-await.test
+++ b/test-data/unit/check-async-await.test
@@ -335,7 +335,7 @@ async def f() -> None:
     async with C() as y, C() as z:  # type: str, int  # E: Incompatible types in assignment (expression has type "int", variable has type "str")
         pass
 
-    async with C() as a:  # type: int, int  # E: Invalid tuple literal type
+    async with C() as a:  # type: int, int  # E: Syntax error in type annotation # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
         pass
 [builtins fixtures/async_await.pyi]
 [typing fixtures/typing-full.pyi]
diff --git a/test-data/unit/check-fastparse.test b/test-data/unit/check-fastparse.test
--- a/test-data/unit/check-fastparse.test
+++ b/test-data/unit/check-fastparse.test
@@ -433,5 +433,5 @@ def update_state(tid,                # type: int
                  ):         # type: (...) -> str
     pass
 [out]
-main:3: error: Invalid tuple literal type
+main:3: error: Syntax error in type annotation
 main:3: note: Suggestion: Is there a spurious trailing comma?
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -2212,7 +2212,7 @@ foo(y=2)  # E: Missing positional arguments
 def dec(f): pass
 
 @dec
-def test(a: str) -> (str,): # E: Invalid tuple literal type # N: Suggestion: Is there a spurious trailing comma?
+def test(a: str) -> (str,): # E: Syntax error in type annotation # N: Suggestion: Is there a spurious trailing comma?
     return None
 
 [case testReturnTypeLineNumberNewLine]
diff --git a/test-data/unit/check-statements.test b/test-data/unit/check-statements.test
--- a/test-data/unit/check-statements.test
+++ b/test-data/unit/check-statements.test
@@ -182,7 +182,7 @@ for z in x:  # type: int
 for w in x:  # type: Union[int, str]
     reveal_type(w)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
 
-for v in x:  # type: int, int  # E: Invalid tuple literal type
+for v in x:  # type: int, int  # E: Syntax error in type annotation # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
     pass
 [builtins fixtures/list.pyi]
 
@@ -191,7 +191,7 @@ for v in x:  # type: int, int  # E: Invalid tuple literal type
 from typing import List, Tuple
 x = []  # type: List[Tuple[int, int]]
 
-for y in x:  # type: int, int  # E: Invalid tuple literal type
+for y in x:  # type: int, int  # E: Syntax error in type annotation # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
     pass
 
 for z in x:  # type: Tuple[int, int]
@@ -1331,7 +1331,7 @@ with A() as a:  # type: int
 with A() as b:  # type: str  # E: Incompatible types in assignment (expression has type "int", variable has type "str")
     pass
 
-with A() as c:  # type: int, int  # E: Invalid tuple literal type
+with A() as c:  # type: int, int  # E: Syntax error in type annotation # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
     pass
 
 with A() as d:  # type: Union[int, str]
@@ -1386,7 +1386,7 @@ with A() as a, A() as (b, c), B() as d:  # type: Tuple[int, int], (int, int), st
 with A() as e, A() as (f, g), B() as h:  # type: Tuple[int, int], Tuple[int, int], str
     pass
 
-with A() as i, A() as (j, k), B() as l:  # type: (int, int), (int, int), str  # E: Invalid tuple literal type
+with A() as i, A() as (j, k), B() as l:  # type: (int, int), (int, int), str  # E: Syntax error in type annotation # N: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
     pass
 
 with A(), A(), B() as m, A() as n, B(), B() as o:  # type: int, Tuple[int, int]  # E: Incompatible number of types for `with` targets
@@ -1615,4 +1615,3 @@ N = TypedDict('N', {'x': int})
 [builtins fixtures/dict.pyi]
 [typing fixtures/typing-full.pyi]
 [out]
-
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -3872,9 +3872,11 @@ def inner():
     # type: () -> (str, int)
     return 'lol', 10
 [out]
-a.py:1: error: Invalid tuple literal type
+a.py:1: error: Syntax error in type annotation
+a.py:1: note: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
 ==
-a.py:2: error: Invalid tuple literal type
+a.py:2: error: Syntax error in type annotation
+a.py:2: note: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
 
 [case testImplicitTuple3]
 import a
diff --git a/test-data/unit/semanal-types.test b/test-data/unit/semanal-types.test
--- a/test-data/unit/semanal-types.test
+++ b/test-data/unit/semanal-types.test
@@ -1474,7 +1474,8 @@ MypyFile:1(
 [case testTupleExpressionAsType]
 def f(x: (int, int)) -> None: pass
 [out]
-main:1: error: Invalid tuple literal type
+main:1: error: Syntax error in type annotation
+main:1: note: Suggestion: Use Tuple[T1, ..., Tn] instead of (T1, ..., Tn)
 
 [case tesQualifiedTypeNameBasedOnAny]
 from typing import Any