-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
type inference is fundementally broken when dealing with class hierarchies #18737
Comments
ok if that wasn't obviously a bug enough (because technically a B is an A: class A:
def a_say(self):
print('a')
class B(A):
def b_say(self):
print('b')
class C(B):
def c_say(self):
print('c')
def foo() -> A | B | C:
return random.choice([A, B, C])()
def main():
x: A | B | C = foo()
reveal_type(x) # mypy Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if hasattr(x, "b_say"):
x.b_say()
reveal_type(x) # mypy Revealed type is "mypy_ex.A"``` |
Similar 'degrade to common superclass' behavior where it directly breaks something. class B:
pass
class E:
pass
def foo() -> B | E:
return random.choice([B, E])() # error: Incompatible return value type (got "object", expected "B | E")``` |
You are reporting a number of different samples and I'm not sure there's anything actionable here. Your first post says The second post shows the most confusing behavior and perhaps there's something we can change there. Here's a mypy playground with a few more reveal_types: https://mypy-play.net/?mypy=latest&python=3.12&gist=e62cbd2edd0a255ffe19604e5ff4f79d. I think the The third post is an example of #12056. |
Hi JelleZilstra, thanks for looking at this. I understand that B is a subclass of A, so a B 'is' an A. However under that logic just always infer the type I would propose that any time a static analyzer could logically infer a type, but fails to do so, that is a defect. The goal has to be to get as close to the type revealed at runtime as possible. In all my examples a it is relatively trivial to determine the correct type. All Mypy has to do is not unify to the superclass so aggressively, and in all these cases there's no logical reason for it to unify at all. |
In that case here is another bug. As you said, you can't narrow to B because there may be an unknown type somewhere that also matches. Here B has the Protocol BSay, but there could be any number of other subclasses of A that also support BSay so narrowing is incorrect. I think you have to pick one or the other of these as wrong because the logic is identical. @runtime_checkable
class BSay(Protocol):
def b_say(self): ...
class A:
def a_say(self):
print("a")
class B(A):
def b_say(self):
print("b")
class C(B):
def c_say(self):
print("c")
def foo() -> A | B | C:
return random.choice([A, B, C])()
def main():
x: A | B | C = foo()
reveal_type(x) # mypy Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if isinstance(x, BSay):
x.b_say()
reveal_type(x) # Revealed type is "mypy_ex.B" |
The more I play with this, the more it's obvious that this is not a philosophical decision. Mypy seems to 'panic' in lots of situations and lose track of the Union. There's no theory of types that makes the following make sense: def main():
x: A | B | C = foo()
reveal_type(x) # mypy Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if hasattr(x, "a_say"):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if hasattr(x, "a_say") or hasattr(x, "__dict__"):
reveal_type(x) # Revealed type is "mypy_ex.A"
if hasattr(x, "a_say"):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]" |
Here's another one. Why does having ASay let you be A | B | C, but BSay is not B | C? This is silly and obviously inconsistent and a bug. @runtime_checkable
class ASay(Protocol):
def a_say(self): ...
@runtime_checkable
class BSay(Protocol):
def b_say(self): ...
def main():
x: A | B | C = foo()
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if isinstance(x, BSay):
reveal_type(x) # Revealed type is "mypy_ex.B"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]" |
Removing A from contention brings C back! 😆 import random
from typing import Protocol
from typing import reveal_type
from typing import runtime_checkable
class A:
def a_say(self):
print("a")
class B(A):
def b_say(self):
print("b")
class C(B):
def c_say(self):
print("c")
def foo() -> B | C:
return random.choice([B, C])()
@runtime_checkable
class ASay(Protocol):
def a_say(self): ...
@runtime_checkable
class BSay(Protocol):
def b_say(self): ...
def main():
x: B | C = foo()
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if isinstance(x, BSay):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if __name__ == "__main__":
for _ in range(10):
main()``` |
Ok not to just nuke this from orbit, but: def main():
x: B | C = foo()
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if isinstance(x, BSay):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if hasattr(x, "a_say"):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]"
if hasattr(x, "b_say"):
reveal_type(x) # Revealed type is "Union[mypy_ex.B, mypy_ex.C]" def main():
x: A | B | C = foo()
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if isinstance(x, ASay):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if isinstance(x, BSay):
reveal_type(x) # Revealed type is "mypy_ex.B"
if hasattr(x, "a_say"):
reveal_type(x) # Revealed type is "Union[mypy_ex.A, mypy_ex.B, mypy_ex.C]"
if hasattr(x, "b_say"):
reveal_type(x) # Revealed type is "mypy_ex.A"``` |
@justinvanwinkle, the type There are cases where mypy uses a "join" operator rather than a "union" operator and therefore produces a type that is wider than necessary resulting in downstream false positive errors. This has been discussed in detail in other places including the summary issue that Jelle links to above. So, when you point out that mypy sometimes uses a join rather than a union, you're making a valid point, but it's one that has been discussed at length already. You're not adding anything new to the conversation. The mypy maintainers are aware of this and have been making changes over time to convert joins to unions, and they will likely continue to convert more cases over time. Each of these changes has potential backward compatibility impacts for mypy users, so they are being cautious in how they approach it. |
In practice they aren't equivalent at all. They could be but mypy's inference treats them differently, so the right hand is disagreeing with the left hand here.
This is simply incorrect. Mypy has removed the two types from the Union where it would be correct, and left the only type where it is not correct. B can be used in situations where A can be used, but A cannot always be used in situations where B can be used. I would direct you to the following article which does a good job of explaining this concept Covariance and Contravariance If I have a function like |
Bug Report
mypy gives an incorrect list of possible types. Although B is a subclass of A, B is not A.
To Reproduce
Expected Behavior
Actual Behavior
Your Environment
mypy --check-untyped-defs --config-file=/dev/null mypy_ex.py
mypy.ini
(and other config files):The text was updated successfully, but these errors were encountered: