Repository: python/mypy

Issue:
Regression: isinstance(x, AliasOfTuple) is not supported
Example:
```Python
T = (int, int)
x : object
if isinstance(x, T):
    reveal_type(x)  # Revealed type is 'builtins.object'
    x + x  # E: Unsupported left operand type for + ("object")
```

This was introduced in #2995


Discussion:
Also @pkch please have a look.
Yes, I will fix it. It's not really a type alias though, since `(int, int)` is not a type, it's a regular python object. A type alias would be

```
T = Tuple[int, int]
x : object
if isinstance(x, T):
    reveal_type(x)  # Revealed type is 'builtins.object'
    x + x  # E: Unsupported left operand type for + ("object")

```
and I don't think we intend to support that correct?
I have used `T = (types ...)` in the PR precisely in order to avoid testing against union, but also avoid repeating long, possibly changing, list of types. Maybe `isinstance(x, Union[simple types])` should be OK?

This way or another the decision needs to be conscious.
I think it's good to keep typing-related stuff separated from regular python code; otherwise the logic becomes very complicated - both for Python developers and for `mypy`.

Originally, this separation was very clean: names defined in `typing` could only be used in annotations. Later, type aliases were introduced, and so `typing` domain leaked through to the regular python code in the form of assignments. But it was worth it because type aliases are very useful.

Now, unless I'm missing something, "type concepts" (i.e., names imported from `typing` and type aliases) can only be used in annotations or on the RHS of assignment expressions where LHS is a simple identifier.

I think adding more contexts where "type concepts" can be used should be done rarely and with a very strong use case. In particular, I don't think `isinstance(x, Union[A,B])` is worth allowing because it's easy to rewrite it as an `or` statement.
Actually, I just realized that there is one more context where "type concepts" can currently be used: in the second argument of `isinstance` (and `issubclass`); this is both valid and useful:
```
x: Union[List[str], List[int]]
if isinstance(x, List[str]): ...
```

Given that, I don't see any problem with supporting more complicated type expressions, if it seems useful (including `isinstance(x, Union[A, B])` which isn't currently allowed).
That code fails at runtime though (`TypeError: Parameterized generics cannot be used with class or instance checks`), so what's the point of supporting it in mypy?
@JelleZijlstra Oh lol riiiiight, my own PR #3040 is supposed to catch this statically :) I forgot.

So, then I am going back to my earlier suggestion: perhaps it's better not to expand the contexts where "type concepts" are allowed beyond the current (type annotations and RHS of simple assignments).

Resolving patch:
diff --git a/mypy/binder.py b/mypy/binder.py
--- a/mypy/binder.py
+++ b/mypy/binder.py
@@ -110,8 +110,7 @@ def _get(self, key: Key, index: int=-1) -> Type:
         return None
 
     def put(self, expr: Expression, typ: Type) -> None:
-        # TODO: replace with isinstance(expr, BindableTypes)
-        if not isinstance(expr, (IndexExpr, MemberExpr, NameExpr)):
+        if not isinstance(expr, BindableTypes):
             return
         if not expr.literal:
             return
@@ -203,8 +202,7 @@ def assign_type(self, expr: Expression,
                     type: Type,
                     declared_type: Type,
                     restrict_any: bool = False) -> None:
-        # TODO: replace with isinstance(expr, BindableTypes)
-        if not isinstance(expr, (IndexExpr, MemberExpr, NameExpr)):
+        if not isinstance(expr, BindableTypes):
             return None
         if not expr.literal:
             return
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2768,8 +2768,16 @@ def flatten(t: Expression) -> List[Expression]:
         return [t]
 
 
+def flatten_types(t: Type) -> List[Type]:
+    """Flatten a nested sequence of tuples into one list of nodes."""
+    if isinstance(t, TupleType):
+        return [b for a in t.items for b in flatten_types(a)]
+    else:
+        return [t]
+
+
 def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]:
-    all_types = [type_map[e] for e in flatten(expr)]
+    all_types = flatten_types(type_map[expr])
     types = []  # type: List[TypeRange]
     for type in all_types:
         if isinstance(type, FunctionLike) and type.is_type_obj():


Test patch:
diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test
--- a/test-data/unit/check-isinstance.test
+++ b/test-data/unit/check-isinstance.test
@@ -1412,3 +1412,19 @@ def f(x: Union[int, A], a: Type[A]) -> None:
 
 [builtins fixtures/isinstancelist.pyi]
 
+
+[case testIsinstanceVariableSubstitution]
+T = (int, str)
+U = (list, T)
+x: object = None
+
+if isinstance(x, T):
+    reveal_type(x)  # E: Revealed type is 'Union[builtins.int, builtins.str]'
+
+if isinstance(x, U):
+    reveal_type(x)  # E: Revealed type is 'Union[builtins.list[Any], builtins.int, builtins.str]'
+
+if isinstance(x, (set, (list, T))):
+    reveal_type(x)  # E: Revealed type is 'Union[builtins.set[Any], builtins.list[Any], builtins.int, builtins.str]'
+
+[builtins fixtures/isinstancelist.pyi]