Repository: python/mypy

Issue:
Improve unification of conditional expressions
I have this snippet of code

``` python
from typing import Any,List

class Node:
    def __init__(self, value:Any, *, children:List['Node']=None) -> None:
        self.value = value
        self.children = [] if children is None else children # type: List[Node]
```

if I use mypy I get

``` bash
Tree.py: note: In member "__init__" of class "Node":
Tree.py:10: error: Incompatible types in assignment (expression has type "object", variable has type List[Node])
```

I _think_ it's something related to the children parameter setted to None, but the problem is... how do I specify that yes, maybe the children parameter can be None, but self.children, by construction is always a List of Nodes?



Discussion:
This looks like the type of the expression `([] if ... else children)`,
where children has type `List[Node]`, is inferred as `object` whereas it
should really be inferred as `List[Node]`. We should fix this. In the
meantime you can work around it by replacing `[]` with `cast(List[Node],
[])`. But that's a very verbose solution and mypy should do better.

The if..else expression doesn't propagate the type context (in your example `List[Node]`) to the child expressions. Propagating the context would let mypy infer type `List[Node]` for the left operand.

A related way to fix this would be to use the type of the right operand as the type context for the first expression, in case the latter has an incomplete inferred type. This would have the benefit of not requiring a type annotation.

I think that you could also work around the problem by rearranging the expression:

```
self.children = children if children is not None else []
```

Well, in this case if the unification of empty list and List[Node] was the
latter we wouldn't even need to propagate the context.

--Guido (mobile)

I keep running into this. The type of (a if x else b) where a and b don't have the same type often degenerates to object or some other useless type rather than the intersection of the two types. Latest example:

```
class A: pass
class B:
    def foo(self): pass
class C(A, B): pass  # No error if we switch this to C(B, A)
a = B() if True else C()
a.foo()  # E: "object" has no attribute "foo"
```

Also, e.g.in Python 2 mode:

```
float(Decimal(0) if True else 0)  # E: No overload variant of "float" matches argument types [builtins.object]
```

In Python 3 mode this gives

```
error: Argument 1 to "float" has incompatible type "object"; expected "Union[SupportsFloat, str, bytes]"
```

I'm going to have a crack at this.


Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1,6 +1,7 @@
 """Mypy type checker."""
 
 import itertools
+import contextlib
 
 from typing import (
     Any, Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple
@@ -295,6 +296,12 @@ def push_loop_frame(self):
     def pop_loop_frame(self):
         self.loop_frames.pop()
 
+    def __enter__(self) -> None:
+        self.push_frame()
+
+    def __exit__(self, *args: Any) -> None:
+        self.pop_frame()
+
 
 def meet_frames(*frames: Frame) -> Frame:
     answer = Frame()
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1,6 +1,6 @@
 """Expression type checker. This file is conceptually part of TypeChecker."""
 
-from typing import cast, List, Tuple, Callable, Union, Optional
+from typing import cast, Dict, List, Tuple, Callable, Union, Optional
 
 from mypy.types import (
     Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
@@ -27,7 +27,7 @@
 from mypy import messages
 from mypy.infer import infer_type_arguments, infer_function_type_arguments
 from mypy import join
-from mypy.subtypes import is_subtype
+from mypy.subtypes import is_subtype, is_equivalent
 from mypy import applytype
 from mypy import erasetype
 from mypy.checkmember import analyze_member_access, type_object_type
@@ -1406,6 +1406,7 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No
     def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
         cond_type = self.accept(e.cond)
         self.check_not_void(cond_type, e)
+        ctx = self.chk.type_context[-1]
 
         # Gain type information from isinstance if it is there
         # but only for the current expression
@@ -1414,26 +1415,36 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
             self.chk.type_map,
             self.chk.typing_mode_weak())
 
-        self.chk.binder.push_frame()
-
-        if if_map:
-            for var, type in if_map.items():
-                self.chk.binder.push(var, type)
+        if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx)
 
-        if_type = self.accept(e.if_expr)
+        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)
 
-        self.chk.binder.pop_frame()
-        self.chk.binder.push_frame()
+            # If it would make a difference, re-analyze the left
+            # branch using the right branch's type as context.
+            if ctx is None or not is_equivalent(else_type, ctx):
+                # TODO: If it's possible that the previous analysis of
+                # the left branch produced errors that are avoided
+                # using this context, suppress those errors.
+                if_type = self.analyze_cond_branch(if_map, e.if_expr, context=else_type)
 
-        if else_map:
-            for var, type in else_map.items():
-                self.chk.binder.push(var, type)
+        else:
+            # Analyze the right branch in the context of the left
+            # branch's type.
+            else_type = self.analyze_cond_branch(else_map, e.else_expr, context=if_type)
 
-        else_type = self.accept(e.else_expr, context=if_type)
+        res = join.join_types(if_type, else_type)
 
-        self.chk.binder.pop_frame()
+        return res
 
-        return join.join_types(if_type, else_type)
+    def analyze_cond_branch(self, map: Optional[Dict[Node, Type]],
+                            node: Node, context: Optional[Type]) -> Type:
+        with self.chk.binder:
+            if map:
+                for var, type in map.items():
+                    self.chk.binder.push(var, type)
+            return self.accept(node, context=context)
 
     def visit_backquote_expr(self, e: BackquoteExpr) -> Type:
         self.accept(e.expr)


Test patch:
diff --git a/mypy/test/data/check-inference.test b/mypy/test/data/check-inference.test
--- a/mypy/test/data/check-inference.test
+++ b/mypy/test/data/check-inference.test
@@ -1639,3 +1639,60 @@ a1 = D1() if f() else D2()
 a1.foo1()
 a2 = D2() if f() else D1()
 a2.foo2()
+
+[case testUnificationEmptyListLeft]
+def f(): pass
+a = [] if f() else [0]
+a() # E: List[int] not callable
+[builtins fixtures/list.py]
+
+[case testUnificationEmptyListRight]
+def f(): pass
+a = [0] if f() else []
+a() # E: List[int] not callable
+[builtins fixtures/list.py]
+
+[case testUnificationEmptyListLeftInContext]
+from typing import List
+def f(): pass
+a = [] if f() else [0] # type: List[int]
+a() # E: List[int] not callable
+[builtins fixtures/list.py]
+
+[case testUnificationEmptyListRightInContext]
+# TODO Find an example that really needs the context
+from typing import List
+def f(): pass
+a = [0] if f() else [] # type: List[int]
+a() # E: List[int] not callable
+[builtins fixtures/list.py]
+
+[case testUnificationEmptySetLeft]
+def f(): pass
+a = set() if f() else {0}
+a() # E: Set[int] not callable
+[builtins fixtures/set.py]
+
+[case testUnificationEmptyDictLeft]
+def f(): pass
+a = {} if f() else {0: 0}
+a() # E: Dict[int, int] not callable
+[builtins fixtures/dict.py]
+
+[case testUnificationEmptyDictRight]
+def f(): pass
+a = {0: 0} if f() else {}
+a() # E: Dict[int, int] not callable
+[builtins fixtures/dict.py]
+
+[case testUnificationDictWithEmptyListLeft]
+def f(): pass
+a = {0: []} if f() else {0: [0]}
+a() # E: Dict[int, List[int]] not callable
+[builtins fixtures/dict.py]
+
+[case testUnificationDictWithEmptyListRight]
+def f(): pass
+a = {0: [0]} if f() else {0: []}
+a() # E: Dict[int, List[int]] not callable
+[builtins fixtures/dict.py]