Repository: python/mypy
Pull request: Fix error when assigning a ternary with an empty collection to a union type variable 
resolves #7780 

Problem statement:
Error when assigning a ternary with an empty collection to a union type variable
Hello,

the following code:
```python
from typing import List, Union
import sys
p : Union[int, List[int]] = 1 if len(sys.argv) else []
```

incorrectly generates the following error:

    t.py:3: error: Incompatible types in assignment (expression has type "Union[int, List[<nothing>]]", variable has type "Union[int, List[int]]")

Adding an integer in the list makes the error disappear. I first observed this issue with a dictionary:
```python
from typing import Dict, Union
import sys
p : Union[int, Dict[int, int]] = 1 if len(sys.argv) else {}
```
Error:

    t.py:3: error: Incompatible types in assignment (expression has type "Union[int, Dict[<nothing>, <nothing>]]", variable has type "Union[int, Dict[int, int]]")

It seems `<nothing>` should be matched with any type in type parameters in unions.

Hints from issue discussion:
Please, take a look at [this](https://github.com/python/typing/issues/157) topic. Perhaps mypy should consider an empty list like an object that fits `List[int]`, but unfortunately it doesn't. 
I think the best solution for you would be use `Any` type: 

`p : Union[int, List[Any]] = 1 if len(sys.argv) else []`
I worked around this issue by getting rid of the ternary condition and using a regular if statement instead.
Another possible workaround could be using a covariant collection like `Sequence`. I however think mypy can do better here. We can probably pass through the type context in case of the ternary expressions.
This actually may be not hard to fix, one just needs to update the logic in `mypy/checkexpr.py` in `visit_conditional_expr()` to try the original context `ctx` for the else expression.

Commit message: modify ExpressionChecker.visit_conditional_expr, add testcase

Patch to review:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 27f462b7d00f..e2a6b1af0edd 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -3534,6 +3534,8 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
 
         if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx)
 
+        # Analyze the right branch using full type context and store the type
+        full_context_else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx)
         if not mypy.checker.is_valid_inferred_type(if_type):
             # Analyze the right branch disregarding the left branch.
             else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx)
@@ -3556,7 +3558,7 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
         #
         # TODO: Always create a union or at least in more cases?
         if isinstance(get_proper_type(self.type_context[-1]), UnionType):
-            res = make_simplified_union([if_type, else_type])
+            res = make_simplified_union([if_type, full_context_else_type])
         else:
             res = join.join_types(if_type, else_type)
 
diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test
index 39dddb77c332..8f6e91cfd625 100644
--- a/test-data/unit/check-unions.test
+++ b/test-data/unit/check-unions.test
@@ -1004,3 +1004,9 @@ do_thing_with_enums(boop)  # E: Argument 1 to "do_thing_with_enums" has incompat
                            # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
                            # N: Consider using "Sequence" instead, which is covariant
 [builtins fixtures/isinstancelist.pyi]
+
+[case testAssignUnionWithTenaryExprWithEmptyCollection]
+from typing import Dict, List, Union
+x: Union[int, List[int]] = 1 if "" else []
+y: Union[int, Dict[int, int]] = 1 if "" else {}
+[builtins fixtures/isinstancelist.pyi]


Code review comments:
diff_hunk: @@ -1004,3 +1004,9 @@ do_thing_with_enums(boop)  # E: Argument 1 to "do_thing_with_enums" has incompat
                            # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
                            # N: Consider using "Sequence" instead, which is covariant
 [builtins fixtures/isinstancelist.pyi]
+
+[case testAssignUnionWithTenaryExprWithEmptyCollection]
+from typing import Dict, List, Union
+x: Union[int, List[int]] = 1 if "" else []
+y: Union[int, Dict[int, int]] = 1 if "" else {}
path: test-data/unit/check-unions.test
text: @user1:
Could you please also add the opposite conditional expressions to the test (since the current inference logic seems to be a bit asymmetric):
```
x: Union[int, List[int]] = [] if bool() else 1
y: Union[int, Dict[int, int]] = {} if bool() else 1
```

@author:
Very reasonable to make it symmetric. See 47f9f25

diff_hunk: @@ -3534,6 +3534,8 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
 
         if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx)
 
+        # Analyze the right branch using full type context and store the type
+        full_context_else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx)
         if not mypy.checker.is_valid_inferred_type(if_type):
             # Analyze the right branch disregarding the left branch.
             else_type = self.analyze_cond_branch(else_map, e.else_expr, context=ctx)
path: mypy/checkexpr.py
text: @user1:
It looks like can replace this with `else_type = full_context_else_type` to avoid making exactly the same call twice.

diff_hunk: @@ -1004,3 +1004,9 @@ do_thing_with_enums(boop)  # E: Argument 1 to "do_thing_with_enums" has incompat
                            # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
                            # N: Consider using "Sequence" instead, which is covariant
 [builtins fixtures/isinstancelist.pyi]
+
+[case testAssignUnionWithTenaryExprWithEmptyCollection]
+from typing import Dict, List, Union
+x: Union[int, List[int]] = 1 if "" else []
path: test-data/unit/check-unions.test
text: @user1:
I think it is safer to use a function call like `bool()` as the condition, so that mypy will not infer the condition as always false if we add such feature in future.