-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Do not treat match value patterns as isinstance checks
#20146
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
base: master
Are you sure you want to change the base?
Conversation
This comment has been minimized.
This comment has been minimized.
|
I'm frankly not sure what part of the core team to ping here - reviewers of the original PR, @JukkaL? @randolf-scholz please also take a look if you have time, IIRC some recent PRs of yours were related to pattern matching? Primer finds nothing, apparently |
Well, many popular projects still support 3.9, but match-case requires at least 3.10, so that's not surprising. |
|
One thing that might be worth testing is if it interacts correctly with unions types and union patterns, e.g. from typing import Literal
def test1(x: Literal[1,2,3]) -> None:
match x:
case 1:
reveal_type(x)
case other:
reveal_type(x)
def test2(x: Literal[1,2,3]) -> None:
match x:
case 1:
reveal_type(x)
case 2:
reveal_type(x)
case 3:
reveal_type(x)
case other:
assert_never(x)
def test3(x: Literal[1,2,3]) -> None:
match x:
case 1 | 3:
reveal_type(x)
case other:
reveal_type(x)and possibly the same with enum arguments / enum patterns. |
|
I don't want to add any enum tests here, they won't be representative due to #19594 - I'd prefer to add |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
| match choice: | ||
| case b.One: | ||
| reveal_type(choice) # N: Revealed type is "def () -> b.One" | ||
| reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this narrow the type variable upper bound? (You can check e.g. by adding an attribute to only one class and accessing it here). If not, I would say this is a regression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not, but I won't really call that a regression - we do not support the equivalent equality case either.
[case testEqualsTypeObjectTypeVar]
# flags: --warn-unreachable
from typing import TypeVar
import b
T_Choice = TypeVar("T_Choice", bound=b.One | b.Two)
def switch(choice: type[T_Choice]) -> None:
if choice == b.One: # E: Unsupported left operand type for == (some union)
reveal_type(choice) # N: Revealed type is "type[T_Choice`-1]"
reveal_type(choice.x) # E: "type[T_Choice]" has no attribute "x" \
# N: Revealed type is "Union[builtins.int, Any]"
[file b.py]
class One:
x = 0
class Two: ...
so either it is unsafe or unsupported, but either way does not belong to this PR. I'll open a ticket and try to look into this later.
("unsupported left operand type" is really weird to see here, but it sheds some light on why the match version does not work either)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It only worked before by pure coincidence, isinstance logic is fundamentally wrong thing to apply to value patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isinstance logic is fundamentally wrong thing to apply to value patterns
I understand that, but we need to minimize side effects of the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hauntsaninja IIRC you added this test case. Does this change look OK in context of #19159
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the behavior that #19159 wants unsafe to begin with?
In the original example, if one makes class A(b.One): ..., then switch(A) will at runtime catch the assert_never.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I vaguely remember someone already mentioned we should allow this only if the classes are final. Anyway, I would wait to see if @hauntsaninja has any objections here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @randolf-scholz! It certainly is unsafe, a subclass will not compare equal to a superclass. Could you cross-post this observation to #19159 too, please? Negative narrowing could only work with two @final classes, which is as close to a "corner case we can ignore for now" as possible.
@ilevkivskyi positive narrowing you're asking about would also be unsafe if we pass a Two subclass with a metaclass overriding __eq__:
[case testSomething]
from typing import TypeVar
import b
T_Choice = TypeVar("T_Choice", bound=b.One | b.Two)
def switch(choice: type[T_Choice]) -> None:
match choice:
case b.One:
choice.x # AttributeError at runtime
class Mcs(type):
def __eq__(self, other): return True
class BadTwo(b.Two, metaclass=Mcs): ...
switch(BadTwo)
[file b.py]
class One:
x = 0
class Two: ...and I don't remember how custom mcs __eq__ is handled - does mypy consider that off-limits?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can assume people don't do this :-) There are other places where user-defined __eq__ would cause a false positive, in those cases we are careful, but we can accept this false negative IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think the change in this PR is good
I don't think losing the accidental positive narrowing here will regress much (but could be good feature to add positive narrowing for this in the future — we can ignore metaclass __eq__).
(and yeah, points on negative narrowing are correct, I think we can just close #19159 )
Fixes #20358.
As discovered by @randolf-scholz in #20142,
mypytreats value patterns in match statement in a completely wrong way.From PEP 622:
Existing tests for the feature are invalid: for example,
must reveal
object, and testtestMatchValuePatternNarrowsasserts the opposite. Here's a runtime example:I have updated the existing tests accordingly.
The idea is that value patterns are essentially equivalent to
if foo == SomeValuechecks, notisinstancechecks modelled byconditional_types_wit_intersection.The original implementation was introduced in #10191.