Repository: python/mypy

Issue:
Unexpected conditional expression type inference change
#7955 caused this example to start generating type errors:

```py
from typing import List, Union

def g(a: List[str], b: bool) -> bool:
    x = a and b
    y: bool
    return x or y  # Incompatible return value type (got "Union[List[str], bool]", expected "bool")
```

Previously, this generated no errors. It's unclear whether this fixed a false negative or whether this is a regression.

@Michael0x2a Do you have an idea of what might be going on?


Discussion:
I would say this is rather a bug (I am however not sure whether it previously worked on purpose or by coincidence).

I think the inferred type of `x` should be like `UnionType(Instance('list', can_be_true=False), Instance('bool'))`, and then the `or` in return statement should eliminate the false-only items from union.
Just to be clear: the new behavior is a bug, or the old behavior is a bug?
The only thing I can think of is the [changes to `LastKnownValueEraser.visit_instance`](https://github.com/python/mypy/pull/7955/files#diff-4fc0111ba884b489b984eec12dc5012fL142): now that we try erasing types if there are any args, that visitor will end up calling `Instance.copy_modified` more frequently -- and it doesn't seem that copy method actually preserves the `can_be_true` or `can_be_false` fields.

I'm not 100% sure, but I feel the new behavior is probably a bug? I tried playing around with the example function, and I could never get it to return a list, so a inferred return type of `bool` is probably want we want.

But maybe the deeper bug is that there's a discrepancy between how we handle `last_known_value` and `can_be_true` and `can_be_false`? It seems we want to erase the latter less aggressively -- so this would probably be yet another binder/"it would be nice if we had a more sophisticated abstract interpreter" issue?

> Just to be clear: the new behavior is a bug, or the old behavior is a bug?

IMO the new behaviour is a false positive.
@Michael0x2a 
> so this would probably be yet another binder/"it would be nice if we had a more sophisticated abstract interpreter" issue?

I think there is a chance a pretty simple fix exists. Will you have time to look into preserving `can_be_true` and `can_be_false` in `Instance.copy_modified()`? If not, I can look into this tomorrow.
Sure, I can take a look.

Resolving patch:
diff --git a/mypy/erasetype.py b/mypy/erasetype.py
--- a/mypy/erasetype.py
+++ b/mypy/erasetype.py
@@ -142,10 +142,13 @@ class LastKnownValueEraser(TypeTranslator):
     def visit_instance(self, t: Instance) -> Type:
         if not t.last_known_value and not t.args:
             return t
-        return t.copy_modified(
+        new_t = t.copy_modified(
             args=[a.accept(self) for a in t.args],
             last_known_value=None,
         )
+        new_t.can_be_true = t.can_be_true
+        new_t.can_be_false = t.can_be_false
+        return new_t
 
     def visit_type_alias_type(self, t: TypeAliasType) -> Type:
         # Type aliases can't contain literal values, because they are


Test patch:
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -343,6 +343,15 @@ b = bool()
 reveal_type(s and b or b)  # N: Revealed type is 'builtins.bool'
 [builtins fixtures/bool.pyi]
 
+[case testRestrictedBoolAndOrWithGenerics]
+from typing import List
+
+def f(a: List[str], b: bool) -> bool:
+    x = a and b
+    y: bool
+    return reveal_type(x or y)  # N: Revealed type is 'builtins.bool'
+[builtins fixtures/list.pyi]
+
 [case testNonBooleanOr]
 c, d, b = None, None, None # type: (C, D, bool)
 if int():