mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Combining two type checks with `and` deduces `Any` if the first is `Any`

Open sth opened this issue 1 year ago • 2 comments

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.

sth avatar Apr 05 '24 08:04 sth

Maybe @ilevkivskyi remembers why the check was implemented this way?

sth avatar May 04 '24 19:05 sth

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

yonilerner avatar Jun 27 '24 22:06 yonilerner