Repository: python/mypy

Issue:
Improve type inference for "Optional[T] or T" expression
Consider this:

``` python
def f(a: Optional[str]) -> str:
    return a or 'default'
```

This should type-check under `--strict-optional`, but currently the type of `a or 'default'` is inferred as `Optional[str]`. It should realize that if a is `None` the right branch is always taken.

Type inference of `or` and `Optional[x]`
When using `--strict-optional`, the type of `a` is incorrectly `Union[None, str]`:

```
from typing import Optional

def f(x: Optional[str]) -> None:
    a = x or 'default'
    reveal_type(a)  # should be str
```
#1698 seems related.



Discussion:
Dupe of #1733?



Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1086,16 +1086,22 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
         left_type = self.accept(e.left, ctx)
 
         if e.op == 'and':
-            right_map, _ = \
+            right_map, left_map = \
                 mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
                                                    self.chk.typing_mode_weak())
         elif e.op == 'or':
-            _, right_map = \
+            left_map, right_map = \
                 mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
                                                    self.chk.typing_mode_weak())
         else:
+            left_map = None
             right_map = None
 
+        if left_map and e.left in left_map:
+            # The type of expressions in left_map is the type they'll have if
+            # the left operand is the result of the operator.
+            left_type = left_map[e.left]
+
         with self.chk.binder.frame_context():
             if right_map:
                 for var, type in right_map.items():
@@ -1105,6 +1111,13 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
 
         self.check_not_void(left_type, context)
         self.check_not_void(right_type, context)
+
+        # If either of the type maps is None that means that result cannot happen.
+        # If both of the type maps are None we just have no information.
+        if left_map is not None and right_map is None:
+            return left_type
+        elif left_map is None and right_map is not None:
+            return right_type
         return UnionType.make_simplified_union([left_type, right_type])
 
     def check_list_multiply(self, e: OpExpr) -> Type:


Test patch:
diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test
--- a/test-data/unit/check-optional.test
+++ b/test-data/unit/check-optional.test
@@ -175,3 +175,73 @@ def f(x: None) -> str: pass
 def f(x: int) -> int: pass
 reveal_type(f(None))  # E: Revealed type is 'builtins.str'
 reveal_type(f(0))  # E: Revealed type is 'builtins.int'
+
+[case testOptionalTypeOrTypePlain]
+from typing import Optional
+def f(a: Optional[int]) -> int:
+    return a or 0
+[out]
+
+[case testOptionalTypeOrTypeTypeVar]
+from typing import Optional, TypeVar
+T = TypeVar('T')
+def f(a: Optional[T], b: T) -> T:
+    return a or b
+[out]
+
+[case testOptionalTypeOrTypeBothOptional]
+from typing import Optional
+def f(a: Optional[int], b: Optional[int]) -> None:
+    reveal_type(a or b)
+def g(a: int, b: Optional[int]) -> None:
+    reveal_type(a or b)
+[out]
+main: note: In function "f":
+main:3: error: Revealed type is 'Union[builtins.int, builtins.None]'
+main: note: In function "g":
+main:5: error: Revealed type is 'Union[builtins.int, builtins.None]'
+
+[case testOptionalTypeOrTypeComplexUnion]
+from typing import Union
+def f(a: Union[int, str, None]) -> None:
+    reveal_type(a or 'default')
+[out]
+main: note: In function "f":
+main:3: error: Revealed type is 'Union[builtins.int, builtins.str]'
+
+[case testOptionalTypeOrTypeNoTriggerPlain]
+from typing import Optional
+def f(a: Optional[int], b: int) -> int:
+    return b or a
+[out]
+main: note: In function "f":
+main:3: error: Incompatible return value type (got "Optional[int]", expected "int")
+
+[case testOptionalTypeOrTypeNoTriggerTypeVar]
+from typing import Optional, TypeVar
+T = TypeVar('T')
+def f(a: Optional[T], b: T) -> T:
+    return b or a
+[out]
+main: note: In function "f":
+main:4: error: Incompatible return value type (got "Optional[T]", expected "T")
+
+[case testNoneOrStringIsString]
+def f() -> str:
+    a = None
+    b = ''
+    return a or b
+[out]
+
+[case testNoneOrTypeVarIsTypeVar]
+from typing import TypeVar
+T = TypeVar('T')
+def f(b: T) -> T:
+    a = None
+    return a or b
+[out]
+
+[case testNoneAndStringIsNone]
+a = None
+b = "foo"
+reveal_type(a and b)  # E: Revealed type is 'builtins.None'