Combining two type checks with `and` deduces `Any` if the first is `Any`
Combining two isinstance checks with and where the first one deduces the type to Any ignores the second one.
To Reproduce
from typing import Any, reveal_type
class A: pass
class B(A): pass
value: Any
if isinstance(value, A) and not isinstance(value, B):
reveal_type(value)
if not isinstance(value, B) and isinstance(value, A):
reveal_type(value)
https://mypy-play.net/?mypy=latest&python=3.12&gist=643066dcf62c1c2b88bb4714032fa8b5
Expected Behavior
Mypy should deduce the type as A in both cases:
main.py:8: note: Revealed type is "__main__.A"
main.py:10: note: Revealed type is "__main__.A"
Actual Behavior
main.py:8: note: Revealed type is "__main__.A"
main.py:10: note: Revealed type is "Any"
Background
This worked before mypy 1.7 and is a regression caused by #16237. The direct reason is that Any is now preferred in some circumstances in and_conditional_maps() (the isinstance in the if):
https://github.com/python/mypy/blob/80190101f68b52e960c22572ed6cc814de078b9c/mypy/checker.py#L7609-L7618
I'm not sure why this is the case or why it is needed to prefer Any here. Intuitively, it should be the other way around. The additional Any doesn't add any new information but it shouldn't destroy the information already known from other sources.
When I remove the or isinstance(get_proper_type(m1[n1]), AnyType) in the above code, only one testcase fails (in testNarrowingWithAnyOps) because it explicitly tests for this Any result, like this:
class C: ...
class D(C): ...
tp: Any
...
c1: C
if isinstance(c1, tp) and isinstance(c1, D):
reveal_type(c1) # N: Revealed type is "Any"
With the changed code this deduces the type as D instead.
Maybe @ilevkivskyi remembers why the check was implemented this way?
I wonder if this is the same bug that causes this:
from typing import Type, Any
class Foo: pass
x: Any
if x and isinstance(x, Foo):
reveal_type(x) # Reveals as Any instead of Foo
https://mypy-play.net/?mypy=master&python=3.11&gist=a89cc8d27cb963b8b5919595f73894b8