-
-
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
Support for python 3.10 match statement #10191
Conversation
Just curious: does our decision to reuse expression nodes as pattern nodes in the Python AST cause any trouble for you? Our intuition is that creating duplicates of all of the expression nodes used by patterns would be less convenient than the current design, but we're open to feedback if not. As I understand it, you transform Python's AST into your own very early on. |
(Also, let me know if you have any questions or want reviews on this. I'm very much looking forward to mypy's support for this feature!) |
@brandtbucher The reuse of expression nodes does create some problems, but it shouldn't be too bad. I currently have two solutions in mind, but haven't decided on which would be better:
Depending on what kind of data will have to be stored in the nodes option 1 may or may not be the only practical one. Reviews and other feedback is always welcome, no matter how early. |
I decided to go with the first option and create separate ast nodes for patterns. This will probably be a bit more work at the beginning, but will lead to better code and prevent problems down the line. So far I created nodes for as-patterns, or-patterns and literal patterns. Sometimes converting from python ast to my pattern ast isn't trivial. |
) This fixes strconv not displaying False and possibly other falsy values. I ran into this problem while writing tests for #10191.
@brandtbucher With the ast conversion phase of this now being finished here's some more feedback on the reuse of expression nodes: I ended up adding nodes for patterns to mypy and, during ast conversion, converting from expression nodes to pattern nodes. This was a bit harder than ast conversion would have been if python had dedicated pattern nodes. Take a mapping pattern for example: case {'k': v}:
pass This is represented as an ast.Dict, which, according to static types, can have arbitrary expressions as keys. When used as a pattern it can only have literal patterns and value patterns (a.k.a. constant expressions and member expressions) as keys though. This leads to me having to check or cast the types in order to make type checking work, when at runtime the invalid values are already caught by the python parser and don't even reach my code. Another problem were literal patterns. A literal pattern, expressed as expression nodes, can be a Constant, UnaryOp or BinOp. At the same time not all Constant, UnaryOp and BinOp expressions are valid literal patterns. This makes conversion quite complicated. I can understand why you went with not duplicating the nodes, but, at least for this usecase, it would have been a lot more comfortable if python would offer separate pattern nodes. |
Thanks for your thoughts! When I find the time, I might draw up a POC branch with dedicated pattern nodes to see how much that complicates or simplifies CPython's internals. If its a clear win both there and here, then we might as well just do it. I'll take a closer look at what you've done here first. As you note, handling for things like mappings and numeric literals might be simplified dramatically. |
I'm currently working on the type-checking portion of this and would like to hear some opinions on how "assigning" to capture variables might work. PEP 634 allows capturing to existing variables. In this case we would have to make sure that the captured value fits the type. PEP 634 also says that captured variables can be used after the match statement, which means that we need to produce a type that works for all match cases. If we use the normal rules then something like this would not be allowed:
Instead two different variable names would have to be used. Alternatively we could type x as the union or join of all the types it captures and narrow it within the case block. This would be more flexible for the user. |
Not a typing expert, but I would expect the type of ...although I can see the possible benefit of requiring an explicit (We're sort of in uncharted territory here, since the names are not scoped to the case block like in many other languages with similar constructs.) |
@freundTech The equivalent nonmatch code would be something like
And that currently produces I feel like @brandtbucher's proposed semantics are more useful though, so I'd prefer to implement that. |
Pyright already supports pattern matching. What does that do? |
I just checked the behaviour in pyright and found the following:
a: List[int] = ...
match a:
case [b, *x]:
reveal_type(b)
case b:
reveal_type(b)
reveal_type(b) I get
This leaves us with three options:
I would prefer 1, because 2 is probably to restrictive and you are much more likely to use variables assigned in if/elif blocks after those blocks than you are to use capture variables after the match, so pyright's behaviour could hide potential errors. In this case variables assigned inside the case blocks should still follow the if/elif behaviour though and the union behaviour should only apply to capture patterns. |
A second point of discussion would be the behaviour of or-patterns. Here we again have the option of either allowing different types and typing the variable as the union of them or enforcing that capture patterns in or-patterns have the same type. Python already enforces that all subpatterns of or-patterns capture the same names, therefore it would be natural to also enforce that they have the same types. Pyright currently does not enforce this. |
Okay, I think it's fine to be inconsistent, and inferring a union when the paths merge seems fine. There are some refinements possible for control flow which will certainly be requested by some users, but we can put those off till later. E.g.
|
This commit introduces patterns. Instead of representing patterns as expressions, like the python ast does, we create new data structures for them. As of this commit data structures for as-patterns, or-patterns and literal patterns are in place
Congrats! Long awaited feature 🎉 |
I filed #12010 about exhaustiveness checking. |
That issue seems to be about noticing that something has been exhaustively checked. But the other direction is very important too - it's important to exhaustively check in many/most cases when there's a limited number of choices (literals, enums, anything that can be narrowed down but has not been narrowed down all the way). I think the two possibilities were to add a "strict" mode check to require literals & enums to error if all possibilities are not exhausted, and the other was to use a If the explicit opt-in is chosen, though, I think |
Why not use If the user wants the |
Let's move further discussion of exhaustiveness to #12010. |
PlatformName = Literal["linux", "macos", "windows"]
PLATFORMS: Final[Set[PlatformName]] = {"linux", "macos", "windows"}
...
if platform == "linux":
cibuildwheel.linux.build(options, tmp_path)
elif platform == "windows":
cibuildwheel.windows.build(options, tmp_path)
elif platform == "macos":
cibuildwheel.macos.build(options, tmp_path)
else:
assert_never(platform) If a new platform is added, all the places this used (like the one above) will be flagged by MyPy as incomplete. If this was written as a match statement, I'd ideally like this to be enforced by the type checker to not contain fallthrough (via a strictness flag in mypy): match platform:
case "linux":
cibuildwheel.linux.build(options, tmp_path)
case "windows":
cibuildwheel.windows.build(options, tmp_path)
case "macos":
cibuildwheel.macos.build(options, tmp_path) Completeness checking for pattern matching is not done at runtime, since that would require checking against the Literal, would slow the program, etc, so it was not included. But it seems quite valid for a strict flag in mypy. If I explicitly didn't want to have completeness here, I could add But if such a flag was not added, and I have to add Quoting https://hakibenita.com/python-mypy-exhaustive-checking:
|
But we could easily make it so. To people who aren't deep into typing,
I don't like the idea of such a strictness flag. It doesn't work for large applications (there's always some part of the code that doesn't follow the convention). It's much more scalable to require explicitly marking a case where you want this using
By some folks, perhaps. But in the end pattern matching was developed separate from type checking concerns (because it was already controversial enough, and tying it to type checking would probably have sunk it). So let's live with the status quo, which is that you must add an explicit |
For what it's worth I believe in most (if not all) codebases I worked with strict exhaustiveness checking (enabled with a linter flag, of course, with a |
Also, I expect there are very few codebases using pattern matching yet, and even if there are, they are not statically typing those yet, since it was just merged. And no one would force them to enable the PS: I do like |
Well, okay, as long as it's a per-module flag. Also note that pyright (the type checker in VS Code's Python extension) has supported match/case for some time now (IIRC since 3.10 beta). We should ask on typing-sig what their experiences are. Re: |
Not fond of special handling of I'd also like to test for an empty union of types elsewhere - like we are already doing with Finally, PS: Though I don't absolutely hate it - catch all case statements are already slightly special in that you can only have one, it has to be at the end, etc. I think the next step might be to summarize for typing-sig and see if anyone else has opinions, experience, or suggestions? Then the results of that can be added to @JukkaL's exhaustiveness issue? Footnotes
|
Please write fewer words. Mypy's original philosophy was to piggyback as much as possible on existing constructs. E.g. Writing |
I just didn't like the special casing inside the final match-all case statement. But I do like the simplicity of how it reads, as long as people don't expect it to be treated specially by mypy elsewhere if they read it here. A little code searching also shows this is often how
Sorry! I know I do tend to write too much. |
I've emailed typing-sig with a (hopefully short) summary, and I'll try to add any relevant information from responses to #12010. |
Edit Issue appears to be addressed in #12010 This may be desired functionality, however, returns from within a match statement are not recognized in my project. from enum import Enum
class CustomEnum(Enum):
a = 0
b = 1
class Foo:
def bar(self, e: CustomEnum, b: bool) -> int:
match (e, b):
case (CustomEnum.a, True):
return 0
case (CustomEnum.b, True):
return 1
case (CustomEnum.a, False):
return 2
case (CustomEnum.b, False):
return 3 Returns: class Foo:
def bar(self, e: CustomEnum, b: bool) -> int:
match (e, b):
case (CustomEnum.a, True):
return 0
case (CustomEnum.b, True):
return 1
case (CustomEnum.a, False):
return 2
case (CustomEnum.b, False):
return 3
case _:
return 4 However, this example will always return 4, even if someone improperly uses the function, however mypy returns the same error class Foo:
def bar(self, e: CustomEnum, b: bool) -> int:
match (e, b):
case (CustomEnum.a, True):
return 0
case (CustomEnum.b, True):
return 1
case (CustomEnum.a, False):
return 2
case (CustomEnum.b, False):
return 3
case _:
return 4
return 5 Apologies if this is intentional. Additional apologies for my lack of PEP8. |
@tayler6000, this is caused by the known issue #12010. |
@freundTech This PR adds support for dataclasses but not attrs? Any plans on that front? |
I noticed this project does not use GitHub releases. What's a good way to track match statement support in mypy? |
I recommend subscribing to this issue: #12021 (or to that effect, any future release planning issue where this gets included, if not 0.940) |
I'm facing issues with match statement supportConsider the following: from typing import Literal, TypeAlias
Expr: TypeAlias = (
tuple[Literal['lit'], int] |
tuple[Literal['var'], str]
)
def square_lits(expr: Expr) -> Expr:
match expr:
case ('lit', x):
return ('lit', square(x))
case _:
return expr
def square(x: int) -> int:
return x * x Here I get an error:
While the following works fine: from typing import Literal, TypeAlias
Expr: TypeAlias = (
tuple[Literal['lit'], int] |
tuple[Literal['var'], str]
)
def square_lits(expr: Expr) -> Expr:
if expr[0] == 'lit':
return ('lit', square(expr[1]))
return expr
def square(x: int) -> int:
return x * x |
@bergkvist Please report a new issue. |
Description
This PR will add support for the python 3.10 match statement to mypy.
Current progress:
Test Plan
Some tests have been added, but they don't pass yet. There will be a lot more tests in the future.
I'm currently developing and testing on cpython 3.10.0b1 and will make sure to update to newer versions every now and then.