16 ms·
IMO the match statement has some very unintuitive behaviour: match status: case 404: return "Not found" not_found = 404 match
by pansa2 6y ago
IMO the match statement has some very unintuitive behaviour:
match status:
case 404:
return "Not found"
not_found = 404
match status:
case not_found:
return "Not found"
The first checks for equality (`status == 404`) and the second performs an assignment (`not_found = status`).
- rwmj 6y agoThat's how structural pattern matching works in ML-like languages, where it's been in use for 40+ years.
- AlexandrB 6y agoDon't those languages typically have immutable variables so you would not be able to rebind a constant by accident?
- deleted 6y ago[deleted]
- the_duke 6y agoThis is usually orthogonal to mutability. In other languages that support match, whether functional or not, you are not changing the value of the variable, but you are shadowing/rebinding the identifier inside the scope of the match clause.
- pansa2 6y agoPython `match` statements do not introduce a new scope. The second example does indeed change the value of `not_found`.
- deleted 6y ago[deleted]
- rwmj 6y agoOh dear, that is indeed very bad.
- mbo 6y agoWhat's the rationale for not introducing a new scope? The only clause I can find in the PEP is > A capture pattern always succeeds. It binds the subject value to the name using the scoping rules for name binding established for named expressions in PEP 572. (Summary: the name becomes a local variable in the closest containing function scope unless there's an applicable nonlocal or global statement.) This seems... incredibly bad.
- magnusmundus 6y agoIt's consistent with how scoping works in loops and `with` clauses. Agreed that it's more problematic here, though.
- pdw 6y ago"Introducing a new scope" is not a concept that exists in Python, you only have function scope and module scope.
- pansa2 6y agoThere are some exceptions to this - for example, the iteration variable(s) in a list comprehension are only in scope within the comprehension.
- pdw 6y agoThanks, I didn't realize that. That makes their choice really baffling.
- _nosewings_ 6y agoThe explanation is that (in Python 3) list comprehensions are really just syntactic sugar for generator expressions. In Python, "scope" is really a synonym for "dictionary attached to some object", so generators can have local variables (since they have their own internal scope), but purely syntactic constructs cannot.
- amirkdv 6y ago
- the_duke 6y agoThat's a very problematic design decision, imo...
- danielscrubs 6y agoDoesn’t other languages have block scope and shadowing is not an issue, but in Python it will reassign instead of shadowing? In my mind that is not well thought out.
- pfalcon 6y agoInterested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.
- baby 6y agoIn Rust no
- deleted 6y ago[deleted]
- yawaramin 6y agoApparently it's reassigning the existing not_found variable from the outer scope, not binding a new not_found variable in an inner scope.
- TrianguloY 6y agoI just wrote a comment with the same complain and similar example! I also think the same as you wrote, the second check will be a hard to spot mistake
- magnusmundus 6y agoI think your example makes it clearer that it won't be a very subtle a bug to find, but rather completely broken behaviour where only the first case is ever triggered. That should be much simpler to test against. Granted, this breaks down if you're matching by the number of values, possibly other cases. To be honest, I feel I like the (sort-of-) "forced namespacing" of matching constants this brings. It should be an easy habit to discipline too, the rule being very simple: "if you don't want to use literals in your matching, you must plug them in an enumeration" Too bad that enumeration can't be a PEP 435 enum without adding an ugly `.value` accessor to each case, though.
- The_rationalist 6y agoWhy is it performing an implicit assignment instead of an equality check?
- SrslyJosh 6y agoA bit part of the appeal of pattern matching in other languages is support for destructuring, where you implicitly assign variables to members of a data structure you're performing a match on. For example, in ML-ish pseudocode: len([]) = 0 len([first|rest]) = 1 + len(rest) That's a trivial example. The second PEP (pattern matching tutorial) has several other examples: https://www.python.org/dev/peps/pep-0636/#matching-multiple-values https://www.python.org/dev/peps/pep-0636/#matching-multiple-... So, if you use a variable in a pattern, it's an implicit assignment. If you use a literal, it's a value to be matched against. I agree that the trivial case (a single variable with no conditions) may be confusing before you know what's going on, but I think the alternative, where a single variable is a comparison but multiple variables (or a structure) is an assignment isn't necessarily better.
- chriswarbo 6y ago> A bit part of the appeal of pattern matching in other languages is support for destructuring, where you implicitly assign variables to members of a data structure you're performing a match on I would clarify that to implicitly introduce variables. Consider the following: $ python3 Python 3.8.6 (default, Dec 28 2020, 20:00:05) [Clang 7.1.0 (tags/RELEASE_710/final)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def foo(x): ... return ([x for x in [1, 2, 3]], x, (lambda x: x+1)(x), x) ... >>> foo(42) ([1, 2, 3], 42, 43, 42) The list comprehension did not assign the variable x to 1, 2 and 3; it introduced a variable called x, but our original (argument) variable is unaffected (since the second result is 42). Likewise, the nested function doesn't assign its argument variable x, it introduces an argument variable x. Whilst this affects that nested function's result (we get 43), it doesn't affect our original variable, since the final result is still 42. This match syntax seems to be assigning, rather than introducing, which is a shame.
- deleted 6y ago[deleted]
- frou_dh 6y agoI think the general criticism of the match statement is just baggage from C overexposure. See the keyword "case" and the brain immediately snaps to thinking we're in a bog-standard C-style switch statement. It's not a switch! Nowhere does it say switch. It's structural pattern matching! EDIT: The lack of block scoped variable thing does seem like a wart right enough.
- deleted 6y ago[deleted]
- brianberns 6y agoOK, but from a functional programming point of view (where structural pattern matching comes from), "case" should bind a value to a name, not mutate the value of an existing variable. That seems nuts to me.
- hmry 6y agoI don't get it, isn't that exactly what it's doing here?
- magnusmundus 6y agoIt's mutating (not shadowing) `not_found` with the value of `status`. That can cause trouble if you rely on `not_found` keeping the initial value later somewhere. Which you would, e.g. with the global constant of `NOT_FOUND`. Honestly I think the issue is so troublesome only if there's a single case to match, though. With more expected cases it should cause pretty obvious bugs (easy to catch).
- deleted 6y ago[deleted]
- wendyshu 6y agoReassigning not mutating
- deleted 6y ago[deleted]
- utborin 6y agoOh, yikes. I understand what's happening here but this is going to bite a lot of people. I might be misreading the grammar but it looks like you can produce the desired effect by using an attribute, e.g. the following would perform an equality check, not an assignment: match status: case requests.codes.not_found: return "Not found" The tutorial seems to confirm this: "This will work with any dotted name (like math.pi). However an unqualified name (i.e. a bare name with no dots) will be always interpreted as a capture pattern"
- deleted 6y ago[deleted]
- TrianguloY 6y agoYes, apparently your example will work correctly. Now think what will happen if you need to move that not_found variable to the same file as that code (so it will no longer be a dotted name). If you do it manually you need to be extra careful, if you use an automatic tool either it will reject the change or will need to create a dummy class or something in the process.
- im3w1l 6y agoThe inconsistency just makes it worse and will lead to even more bugs. I foresee lint rules prohibiting match statements in the near future.
- coldtea 6y agoCouldn't they have used "case" and "capture" clauses instead? match status: case not_found: return "Not found" capture success_code: return "Returned success code: %s" % success_code or the "_ as x" or a myriad other ways to make capture explicit...
- ben509 6y agoA separate statement doesn't compose with complex patterns. I'd prefer always requiring the walrus operator to capture: match response: case (not_found, msg := _): return f"Not found: {msg}" case (error, "No swizzle available"): return "We lack swizzle. :-(" case pair := (_, "Swizzle"): log(f"Unexpected swizzle: {pair}") return f"Swizzle?" case ((code := _), _): return f"Success: {code}"
- antman 6y agoThat looks pretty counter intuitive even after this explanation
- deleted 6y ago[deleted]
- breuleux 6y agoThat's not too bad if it would be a syntax error to either set or shadow an existing variable with the match statement. Apparently it isn't, which is concerning. Personally I think I may have preferred something like: match status: case == not_found: # Check for equality ... match status: case as not_found: # bind status to variable not_found ... At least the former should be an option instead of using a dotted name IMO. You know, a lot of potentially confusing behavior would be avoided if programming languages had the sense to make variables read-only by default and disallow variable shadowing altogether.
- willcipriano 6y agoI've been scanning the docs and this syntax is hard for me to understand, however your version makes sense right on first glance. I'd go with that.
- chriswarbo 6y agoI really like shadowing, since it prevents me making mistakes all over the place by referring to the wrong thing. If I introduce a new name, I have two names cluttering up my namespace, and might pick the wrong one by mistake; for example if I validate 'myInput' to get 'myValidatedInput', later on I can still refer to 'myInput', which would be a mistake, and may end up bypassing the validation. On the other hand, I can shadow 'myInput' with the validated result, meaning that (a) I can no longer refer to the value I no longer want, (b) there's only one suitable "input" in scope, so it's easier to do things correctly, (c) I don't have to juggle multiple names and (d) it's pure and immutable, and hence easier to reason about than statements (like 'del(myInput)' or 'myInput = validate(myInput)'.
- coldtea 6y ago>I really like shadowing, since it prevents me making mistakes all over the place by referring to the wrong thing. If I introduce a new name, I have two names cluttering up my namespace, and might pick the wrong one by mistake; Compared to having two versions of the same name, one shadowing another?
- airstrike 6y agoYes, and this is incredibly disappointing. Couldn't we achieve the same functionality with a little less ambiguity using the following syntax?: not_found = 404 match status: case not_found: return "Not found" case _ as foo: do_something(foo) return "Match anything" it even works for the example in PEP 365 match json_pet: case {"type": "cat", "name": _ as name, "pattern": _ as pattern}: return Cat(name, pattern) case {"type": "dog", "name": _ as name, "breed": _ as breed}: return Dog(name, breed) case _: raise ValueError("Not a suitable pet")
- baby 6y agoThis would be really confusing to anyone who is used with match statements in other languages
- coldtea 6y agoFor 2 seconds until they learn what it means. If anything, it's more explicit what exactly it does. There are 100s of things in Python who would be confusing to people used to other languages, this is not one of them.
- reikonomusha 6y agoThe general tripping up of binding vs mutation vs assignment vs initialization is a pervasive Python issue. This just continues to double down on exacerbating the problem.
- hnlmorg 6y agoComparing garbage syntax to worse syntax and saying “it’s not as bad as that” isn’t the way one improves upon a language.
- coldtea 6y agoWhich is neither here, nor there. This syntax is clearly an improvemen to the PEP syntax. And parent said it's "confusing". Well, it's less confusing than the PEP syntax. Nobody compared it to not adding it at all, or was concerned with improving the language aside from the pattern matching case here.
- _nosewings_ 6y agoThis is how pattern matching works in any language (except for some niche languages that allow nonlinear patterns, like Curry). Many of the comments here reveal a bizarre parochialism.
- pansa2 6y agoIn that case, what’s the correct way to write a clause that only matches if `status` is equal to 404? Do we have to use the integer literal 404 instead of a named integer?
- _nosewings_ 6y agoYes, of course. Again, this is how it works in basically every mainstream language with this feature.
- pansa2 6y agoReally? It’s best practice to use a magic number rather than a name? That breaks one of the most fundamental rules for writing good code.
- _nosewings_ 6y agoIf you really need to compare against a variable, use an "if". The primary benefit of pattern-matching is destructuring. EDIT: It looks like you actually can match against constants with this PEP, as long as you access your constant with a dot (e.g., HttpError.NotFound). This seems like a perfectly reasonable solution to me.
- hnlmorg 6y agoNo it’s not because it’s none obvious and requires a fair amount of boilerplate code. Both of which are usually idioms Python normally tries to avoid. I guarantee you this will trip up a lot of developers who are either learning the language for the first time or who Python isn’t their primary language. Worse still, the kind of bugs this will lead to is valid code with unexpected pattern matching, which is a lot harder to debug than invalid code which gets kicked out with a compiler error.
- dpritchett 6y agoAre you sure the second one isn’t just declaring not_found as a stand-in for 404 so the case statement two lines below can refer to business logic rather than a “magic” constant? I would NOT expect for the line “case not_found:” to reassign the status variable to 404 regardless of what it was before. I can’t see how or why that would be intended behavior.
- deleted 6y ago[deleted]
- pansa2 6y ago> Are you sure the second one isn’t just declaring not_found as a stand-in for 404 That's what it looks like, but no, the refactoring really does change an equality test into an assignment.
- remram 6y agoIt doesn't reassign the 'status' variable, it reassigns the 'not_found" variable.
- deleted 6y ago[deleted]
- mxcrossb 6y agoAssignments going wrong is par for the course in python! I still am surprised it is taught to beginners...
- jonnycomputer 6y agoThat's nuts. Sorry, but ... that's nuts.
- asah 6y agoI will be waiting until pylint detects these sorts of footguns...
- jnxx 6y agoAccording to the specification pointed to by choeger in this comment https://news.ycombinator.com/item?id=26086589 https://news.ycombinator.com/item?id=26086589 , failed matches like that can also assign a value to existing variables, which seems even more problematic to me.
- pfalcon 6y agoThus, will be fixed sooner or later. (Sad it's not fixed right away, but neither me nor you proposed patches.)