Hacker News new | past | comments | ask | show | jobs | submit login
Pattern matching accepted for Python (lwn.net)
654 points by eplanit 17 days ago | hide | past | favorite | 518 comments



[Expanding my comment from another post on this¹]

Direct links to the PEPs themselves, to perhaps save others a little time. Although the LWN write up linked in TFA is a very nice introduction to the topic and its big discussion in the community.

Accepted:

Specification https://www.python.org/dev/peps/pep-0634/

Motivation and Rationale https://www.python.org/dev/peps/pep-0635/

Tutorial https://www.python.org/dev/peps/pep-0636/

Rejected:

Unused variable syntax https://www.python.org/dev/peps/pep-0640/

Explicit Pattern Syntax for Structural Pattern Matching https://www.python.org/dev/peps/pep-0642/

¹ https://news.ycombinator.com/item?id=26073700


At first, I overlooked the excellent LWN writeup on the feature linked in TFA, so I'm linking it here:

https://lwn.net/Articles/838600/


Core dev Larry Hastings [0] puts it well. The cost-benefit case for this complicated language feature is limited.

"I dislike the syntax and semantics expressed in PEP 634. I see the match statement as a DSL contrived to look like Python, and to be used inside of Python, but with very different semantics. When you enter a PEP 634 match statement, the rules of the language change completely, and code that looks like existing Python code does something surprisingly very different. It also adds unprecedented new rules to Python, e.g. you can replace one expression with another in the exact same spot in your code, and if one has dots and the other doesn’t, the semantics of what the statement expresses changes completely. And it changes to yet a third set of semantics if you replace the expression with a single _.

I think the bar for adding new syntax to Python at this point in its life should be set very high. The language is already conceptually pretty large, and every new feature means new concepts one must learn if one is to read an arbitrary blob of someone else’s Python code. The bigger the new syntax, the higher the bar should become, and so the bigger payoff the new syntax has to provide. To me, pattern matching doesn’t seem like it’s anywhere near big enough a win to be worth its enormous new conceptual load."

This while we have elephants in the room such as packaging. Researching best practices to move away from setup.py right now takes you down a rabbit hole of (excellent) blog posts, and yet you still need a setup.py shim to use editable installs, because the new model simply doesn't yet support this fundamental feature.

I can't afford to spend days immersing myself in packaging to the point of writing a PEP, but would help pay someone to do it well. I can see no way to fund packaging efforts directly on the PSF donations page (edit: see comment). It's great to see Pip improving but there is still not even a coherent guide that I can find for packaging using up-to-date best practices. This appears to be because the best practices are currently slightly broken.

[0] https://discuss.python.org/t/gauging-sentiment-on-pattern-ma...


> This while we have elephants in the room such as packaging.

There is a part of me that wonders, at this point, if basically every new addition to the language itself is secretly just a medium for procrastinating on figuring out what to do about setup.py.

I'm not as down on this PEP as some other seem to be, mind. It's just that, when I think about my actual pain points with python, this language addition starts to look like a very, very fancy bike shed.


The people that work on packaging don't overlap much with those that work on the core language. The latter don't seem to care about it much, as far as I can tell.


Have you taken a look at [PEP 517](https://www.python.org/dev/peps/pep-0517/) ? It enables other tools to replace setup.py (e.g., poetry is pretty nice for making easy-to-package-and-publish pure python libraries).


I sort of wonder why I see so much users complaining about package management in Python, meanwhile I'm having a fantastic journey since 2008 with it, with over 50 published open source packages. "Sort of" because I'm suspecting that the users in question just do not want to integrate upstream changes continuously, so, they expect the package manager to help them procrastinating on dependency updates, which has proven to lead to disasters such as npm install, and I'm kind of worried that this is the direction they have been taking.

But I admit I use setupmeta for auto definition of the version, and it just makes setup.py even better, but that's basically the only thing I like to add to setup.py, because it simplifies package publishing scripts. I haven't found any feature in pip to verify the gpg signatures that it allows us to upload packages with (python setup.py sdist upload --sign).

As for pattern matching is not specific to Python, it's available in many other languages and is a joy to use in OCaml, I see no reason why Python would not have pattern matching.


Anything that breaks and changes semantics should not be allowed into the language. Let Python be Python, not a Frankenstein's monster of ideas like C++. If it were an idea that were Pythonic, you would not see the confusing examples I've seen in the comments. C++ is the poster child of trying to do too much with the language and it losing itself due to death-by-committee. It's very sad that Python has started down this road.


We seem to be seeing a paradigm clash. On one side, there are people who are concerned with whether or not a feature is desirable. On the other side, there are people who are concerned with whether or not a feature is desirable and also Pythonic.


> On one side, there are people who are concerned with whether or not a feature is desirable.

The thing is this: You can add something which, in isolation, seems desirable and positive -- but in the greater picture, is a net negative due to the complexity it adds.

People might say that those who do not like the pattern matching syntax are not obliged to use it. But when developing code in longer-running projects, far more code is read than written. Adding syntax, especially with complex edge cases, especially from languages which use concepts that are at the core quite alien to Pythons main concepts, adds a burden which is difficult to justify.


Very much so. I run into this with Clojure. It has so many different ways to skin every cat, each with its own unique blend of quirks, that it can be quite difficult to understand other people's code, and, by extension, use the language in a team setting.

That sort of experience leaves me thinking that this is a very dangerous turn to take for a language whose core ecological niche is, "Easy for professionals who don't have a BS in CS to understand and use productively." Lines 2 and 13 of PEP 20 are core to why Python, of all languages, came to dominate that niche. I am beginning to fear that the Python core team, being composed primarily of software engineers, is ill-equipped to properly understand that.


Yap. It doesn't make sense to destroy the language just to get in a particular feature. You don't need a language to do everything. It needs to be good at everything it's meant to be good at.


I very much want to agree with you. Only that I do not know any more what "Pythonic" is supposed to mean.

One thing that Larry Hastings refers to seems often to be underestimated - readability.

It seems nice to be able to do code golfing and use pattern matching to reduce an expression from maybe 50 lines to 10.

But what matters far more is that one can read code easily, without guessing, and without consulting definitions of edge cases. Even in smaller projects, one will read 10 times more code than one writes. In larger projects and as a senior programmer, that could be a factor 100 or 1000. Not that unusual to work one week through a bug in someone else's code, and fix it by changing a single line. As code becomes more complex, it becomes really important to understand exactly what it means, without guessing. This is key for writing robust, reliable and correct code (and this is perhaps why the ML and functional languages, which stress correctness, tend to be quite compact).

And while it might be satisfying puzzle-solving for smart and easily bored people, like you and me, to write that pattern matching code and reduce its length, it is just not feasible to read through all the PEPs describing surprising syntactic edge cases in a larger code base.


I can only agree. Compared to other languages I find Python increasingly difficult to reason about, mainly due to its dynamicity. If the language complexity increases as well from now on I don't think I will use Python unless absolutely necessary.

Meanwhile, Julia supports pattern matching due to macros: https://github.com/kmsquire/Match.jl


Unfortunately Match.jl has gone unmaintained for a while, but we do have MLStyle.jl

https://github.com/thautwarm/MLStyle.jl

https://thautwarm.github.io/MLStyle.jl/latest/syntax/pattern...


That looks great, thanks.


Some of the english documentation can be quite hard to read, but the code is very useful. Submitting PRs to improve the documentation is welcomed by maintainers.


Pattern matching is the brainchild of ML. Python, being a multi-paradigm language with the strong functional side, missed this simple in concept and powerful in practice language concept.


> Python, being a multi-paradigm language with the strong functional side

Coming back to that, just a reminder that lambdas in Python are still gimped, closures do not work as expected because of -- scoping, and core developers in Python 3 tried to remove with "map" and "filter" tools that are considered quite essential for functional programming.


I actually wish they had done so.

As someone who switches between Python and functional languages, I find Python's "map" and "filter" to be a trap, and have taken to scrupulously avoiding them. The problem is that I expect those functions to be pure, and, in Python, they aren't. They actually can't be, not even in principle, because their domain and range include a core datatype that cannot be interacted with in a pure manner: generators. A generator will change its own state every time you touch it. For example:

  >>> seq = (x for x in range(1, 11))
  >>> list(filter(lambda x: x % 2 == 0, seq))
  [2, 4, 6, 8, 10]
  >>> list(filter(lambda x: x % 2 == 1, seq))
  []
In a language that is a good fit for functional programming, the last statement would return [1, 3, 5, 7, 9], not an empty list. But Python is imperative to the core, so much so that I would argue that trying to use it as a functional language is like trying to drive screws with. . . not even a hammer. A staple gun, maybe?

(Which isn't to say that you can't successfully use some functional techniques in Python. But it's best done in a measured, pragmatic way.)


A good example why immutability by default seems to be the right thing - in Clojure, "seq" would not have been modified by the first filter expression:

    user=> (def seq_ (range 1 11))
    user=> (filter (fn [x] (== (mod x 2) 0)) seq_)
    (2 4 6 8 10)
    user=> (filter (fn [x] (== (mod x 1) 0)) seq_)
    (1 2 3 4 5 6 7 8 9 10)
or more concisely:

    user=> (filter even? seq_)
    (2 4 6 8 10)
    user=> (filter odd? seq_)
    (1 3 5 7 9)

And also an example why it does not work to go and grab one or another desirable feature from a functional language, they need to work together.

> (Which isn't to say that you can't successfully use some functional techniques in Python. But it's best done in a measured, pragmatic way.)

A great example how it is done right is Python's numpy package. The people who created that knew about functional languages and APL (which fits nicely in since Python's predecessor ABC had some APL smell). The obviously knew what they were doing, and created a highly usable combination of a general data type and powerful operations on it.


I found this very surprising so asked a Python-knowledgeable acquaintance who mentioned that this works as expected

    >>> seq = [x for x in range(1,11)]
    >>> list(filter(lambda x: x % 2 == 0, seq))
    [2, 4, 6, 8, 10]
    >>> list(filter(lambda x: x % 2 == 1, seq))
    [1, 3, 5, 7, 9]


Right. Because it's doing two different things. One is working with lists, the other is working with lazy calculations.

A common functional pattern is to do lazy calculations, so that you don't have to store every result in memory all at once. The subtext I'm getting at is that a language that has footguns that make it dangerous (for reasons of correctness, if not performance) to reuse and compose lazy calculations is a language that is fundamentally ill-suited to actual functional programming.

Which is fine! Python's already a great procedural-OO language. It's arguably the best procedural-OO language. Which is a big part of why it's taken over data science, business intelligence, and operations. Those are, incidentally, the domains where I feel just about the least need to program in a functional paradigm. And, in the domains where I do have a strong preference for FP, Python is already a poor choice for plenty of other reasons. (For example, the global interpreter lock eliminates one of the key practical benefits.) No amount of risky, scarring cosmetic surgery is going to change that.


OK I see, the very short sequence in your example was a stand-in for a potentially infinite one. I got nerd-sniped into understanding what was happening as I found the behavior surprising.


It is very surprising.

And that is sort of another layer to why I think that people trying to turn Python into a functional language should just simmer down. A core principle in functional programming is that you should be able to swap out different implementations of an abstraction without changing the correctness of the program. That's not really something that can be enforced by most functional languages, but it's at least a precedent they set in their standard libraries. But Python's standard library has a different way of doing things, and favors different approaches to abstraction. And those conventions make the Python ecosystem a hostile environment for functional programming.

Hylang is another interesting example. It's a cool language. But watching how it evolved is slightly disheartening. It sought to be a lisp for Python, but the compromises they needed to make to get the language to interact well with Python have hurt a lot of its lisp feel, and make it a kind of peculiar language to try to learn as someone with a lisp background. IIRC, Python's lack of block scoping was an elephant in the room for that project, too.


To be fair, that's a foot gun in haskell as well. Using lists non-linearly like this in haskell gives you the correct results but at a 10x performance tax or worse because it can't optimize into a loop anymore.


At least to me, a footgun goes beyond a mere performance gotcha. There's a whole category difference between, "If you do this, the compiler may not be able to optimize your code as well," and, "If you do this, your code may produce incorrect results."


> Python, being a multi-paradigm language with the strong functional side

I would doubt that. Surely, things like Numpy are written in a functional fashion, but Python relies very much on statements, iteration, things not being an expression, almost every named symbol except string and number literals being mutable, and there are no block-scoped name bindings which are essential to functional languages.

And the attempt to add the latter to Python might end in a far bigger train wreck than C++ is already.

Mixing OOP and functional style works, more or less, for Scala, but everyone agrees that Scala is a hugely complex language. And in difference to Python, it has immutable values.

What could turn out better would be to create a new language which runs interoperable in the same VM (much like Clojure runs alongside Java and can call into it). And that new language, perhaps with the file extension ".pfpy", would be almost purely functional, perhaps like a Scheme or Hy, without these dreaded parentheses. That would at least leverage Python's existing libraries.


> This while we have elephants in the room such as packaging. Researching best practices to move away from setup.py right now takes you down a rabbit hole of (excellent) blog posts, and yet you still need a setup.py shim to use editable installs, because the new model simply doesn't yet support this fundamental feature.

You can do editable installs with poetry, I do it every day.

Just run this: \rm -rv dist/; poetry build --format sdist && tar --wildcards -xvf dist/.tar.gz -O '/setup.py' > setup.py && pip3 install --prefix="${HOME}/.local/" --editable .

More details here: https://github.com/python-poetry/poetry/issues/34#issuecomme...


I do editable installs every day with setup.py – what is your point? Poetry is a very interesting third party solution to these issues, but not a best practice. Best practices matter in packaging.


Struggling a bit with Python and JS-related packaging stuff recently, I'm also wondering: in a clean-room environment, what does good Python packaging look like? Is it "get a single binary" stuff?

PyOxidizer feels like the big win, but a part of me wonders if the original sin of allowing aribtrary code execution during installs really stops a "wrapper" tool from getting this right. https://pyoxidizer.readthedocs.io/en/v0.9.0/index.html


The gold standard for dynamic interpreted language package management is Yarn, and to a lesser extent npm. They are both cross platform, allows for easy publishing and building of native code dependencies, and support each package having its own conflicting dependencies, which means you don't have to do SAT solving. Furthermore, the metadata is held outside of the package, so package selection is much quicker than Python which requires downloading the entire library first and parsing the requirements.


Nope, cpan still is the gold standard.

Packages are tested before installation, and there is no need for hacks like pyenv or broken ruby envs. It also plays nicely with vendor packaging. No sat solving needed, deps are resolved dynamically, powered by simple Makefiles, not special baroque and limited declarations syntax.


> support each package having its own conflicting dependencies

So that you don't have to integrate upstream changes continuously... Not a way to build a sane ecosystem if you ask me.

> package selection is much quicker than Python

So is it because it downloads multiple versions of the same dependencies that it's so much slower than pip? Or is there more?


Is there a difference between package management for dynamic/static languages? Because without this arbitrary distinction npm is pretty much around last place on a ranking between package managers.


Did you try the "Sponsor" link in pypi.org ? It will take you there: https://pypi.org/sponsor/

> The Python Software Foundation is receiving $407,000 USD to support work on pip in 2020. Thank you to Mozilla (through its Mozilla Open Source Support Awards) and to the Chan Zuckerberg Initiative for this funding!


They should give that to the meson author instead. He knows what he's doing and has a clue about C/C++.

Really, the whole distutils is a fancy way to produce a simple zip archive and put it in a well know location in site-packages.

meson has the build figured out, it's just a way of installing that archive with a bit of metadata.


Thanks, donated.


This gives the impression that Python, similar to C++. might have entered a competition between popular languages which one can accumulate the most popular features.

Obviously, pattern matching comes from the ML family and functional Lisps like Clojure. What makes it a difficult integration into Python is that in languages such as Rust, OCaml, Haskell, but also Racket and Clojure, almost everything is an expression, and name bindings are, apart from a few exceptions, always scoped. Consequently, pattern matching is an expression, not a statement.

A similar issue is that Python 3 tried to become more "lazy", similar to how Haskell and Clojure are - this is the reason for replacing some list results with generators, which is a subtle breaking change. Lazy evaluation of sequences is nice-to-have on servers but its importance in Haskell and Clojure comes from them being functional languages which are geared towards a "pure" (side-effect-free) style, in which they differ a lot from Python.

My impression is also that over time, Python has really absorbed a huge number of features from functional Lisp dialects. This might seem surprising. Well, here are some things that modern Lisps like SBCL, Schemes and functional languages on the one hand side, and Python 3 do have in common:

* a Read-Eval-Print Loop (REPL)

* strong dynamic typing

* automatic memory management

* memory safety

* exceptions

* a wide choice of built-in data types: lists, strings, vectors, arrays, dictionaries / hash maps, tuples, sets

* keyword arguments and optional arguments in functions

* handling of names in scopes and names spaces

* closures and lambda functions

* list comprehensions

* pattern matching (limited support for tuples and lists in Python)

* Unicode strings

* arbitrarily long integers

* complex numbers

* rational numbers

* number type are part of a hierarchical type hierarchy (numeric tower)

* empty sequences, containers and strings are logically false

* support for threads (but no real parallelism in Python)

* low-level bit operations (a bit limited in Python)

* easy way to call into C code

* type annotations

* if ... else can be used as an expression

* string formatting is a mini language

* hexadecimal, octal and binary literals

* standard functions for functional programming like map and filter

* support for OOP (e.g. by the Common Lisp Object System)

* support for asynchronous execution

What is notably missing from this list is parallelism (as opposed to concurrency); Python simply does not support it in a practical way, while some functional languages do support it extremely well.

The net result of this creeping featuritis appears to be that Python, which is still classified as "beginner friendly", is now actually significantly more complex than a language like Racket or Clojure, perhaps because these features are often not integrated that well. I even think that Rust, while clearly being targeted at a very different domain, is more streamlined and well-composed than Python.

What is also worth mentioning is that these functional languages have seen steady improvements in compilers and performance of generated code, with the result that Rust code is now frequently at least as fast as C code, SBCL is within arms reach of C, and Clojure and Racket are about in the same league as Java - while Python code in comparison continues to be extremely slow:

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...


Has it absorbed features from functional Lisp dialects, or does it just have features in common?

Early Python was inspired by ABC and C. ABC has a Python-like REPL, strong dynamic typing, automatic memory management, memory safety, a nice collection of built-in types, arbitrarily long integers, and rational numbers. C has low-level bit operations, (an easy way to call into C code,) a ternary expression-level if-else operator (and make no mistake, Python's expression-if is a ternary operator that's distinct from the statement-if-else), and hexadecimal and octal number literals.

There is at least a little influence from functional Lisps (Mypy lists Typed Racket as one of its influences), but a lot of what you list was taken from different languages, or is distinctly un-Lisp-like in Python, or was in Python from the start rather than absorbed over time, or is just obvious enough to put in a very high-level language to be reinvented.

It's also important to distinguish between internal complexity and user complexity. Arbitrarily long integers are complex to implement, but easier to use than fixed-length integers. Even features that do have a lot of user-facing complexity can be very easy to use in the common case. Python is hideously complex if you explore all the tiny details, but I'm not sure that it's all that complex to use. But I haven't used Clojure and Racket so I can't really comment on them.

> I even think that Rust, while clearly being targeted at a very different domain, is more streamlined and well-composed than Python.

I think I agree. But Rust has the benefit of only dealing with a measly five years of backward compatibility. Python has accumulated complexity, but the alternative would have been stagnation. If Python hadn't significantly changed since 1996 it would be more streamlined but also dead.

> What is also worth mentioning is that these functional languages have seen steady improvements in compilers and performance of generated code, with the result that Rust code is now frequently at least as fast as C code

I don't think Rust suffers from the issues that make functional languages hard to compile, so that might be a bad example. In Rust code it's unambiguous where memory lives. It has functional features augmenting a procedural model, rather than a functional model that has to be brought down to the level of procedural execution. So it might be "merely" as hard to optimize as C++.


> C has low-level bit operations,

As an unrelated fun fact, Common Lisp has more low-level bit operations than C, such as "and complement of integer a with integer b" or "exclusive nor":

http://www.lispworks.com/documentation/HyperSpec/Body/f_loga...

It also has logcount, which is the counterpart to the implementation-specific popcount() in C.


> but a lot of what you list was taken from different languages, or is distinctly un-Lisp-like in Python, or was in Python from the start rather than absorbed over time, or is just obvious enough to put in a very high-level language to be reinvented.

Here, Python clearly borrows from functional languages. And there are basically two families of functional languages: Strongly and statically typed languages like ML, OCaml, Haskell, Scala, F#, and on the other hand, dynamically typed Lisps and Schemes.

My point is that all these adopted features are present in the latter category.


How many of these features are present in neither statically typed functional languages nor dynamically typed procedural languages?

My impression is that Python has a) a lot of bog-standard dynamic features, and b) a few functional features (like most languages nowadays).

Group a) overlaps with functional Lisps, but no more than with ABC and Perl and Lua, so functional Lisps are not a great reference point.

Group b) overlaps with functional Lisps, but no more than with ML and Haskell, or even modern fundamentally-procedural languages like Kotlin(?) and Rust, so functional Lisps still aren't a great reference point.

It's mostly parallel evolution. It can be interesting to compare Python to functional Lisps because similarities are similarities no matter where they come from.

But I don't think that functional Lisps neatly slot into an explanation as to why Python looks the way it does. In a world where functional Lisps didn't exist Python might not have looked all that different. In a world where ABC and Modula didn't exist Python would have looked very different, if it existed at all.


> Group b) overlaps with functional Lisps, but no more than with ML and Haskell, or even modern fundamentally-procedural languages like Kotlin(?) and Rust, so functional Lisps still aren't a great reference point.

Both of them stem from Lambda calculus. The difference between ML languages and Lisps is the type system. To do functional operations like map, foldl, filter, reduce in compiled ML-style languages with strong static typing, one needs a rather strong and somewhat complex type system. When you try that at home with a weaker type system, like C++ has for example, the result is messy and not at all pleasant to write.

Lisps/Schemes do the equivalent thing with strong dynamic typing, and good Lisp compilers doing a lot of type inference for speed.

> It's mostly parallel evolution. It can be interesting to compare Python to functional Lisps because similarities are similarities no matter where they come from.

Lisps (and, for the field of numerical computation, also APL and its successors) had and continue to have a lot of influence. They are basically at the origin of the language tree of functional programming. The MLs are a notable fork and apart from that there are basically no new original developments. I would not count that other languages like Java or C++ pick up some FP features like lambdas, too.

What's however interesting is the amount of features that Python 3 has now in common with Lisps. Lisps are minimalist languages - the have only a limited number of features which fit together extremely well.

And if all these features adopted were not basically arbitrary, unconnected, and easy to bolt-on, why has Python such a notably bad performance and -- similar as C++ -- such an explosion in complexity?


Map, filter and lambda were originally suggested by a Lisp programmer, so those do show functional Lisp heritage. (I don't know of similar cases.) But they're a small part of the language. Comprehensions are now emphasized more, and they come from Haskell, and probably SETL originally—no Lisp there.

> They are basically at the origin of the language tree of functional programming.

That's fair. But that only covers Python's functional features, which aren't that numerous.

> if all these features adopted were not basically arbitrary, unconnected, and easy to bolt-on

I never said they weren't! I just don't think they're sourced from functional Lisps.

>why has Python such a notably bad performance

Because it's very dynamic, not afraid to expose deep implementation details, and deliberately kept simple and unoptimized. In the words of Guido van Rossum: "Python is about having the simplest, dumbest compiler imaginable."

Even if you wanted to, it's hard to optimize when somewhere down the stack someone might call sys._getframe() and start poking at the variables twenty frames up. That's not quite a language design problem.

PyPy is faster than CPython but it goes to great lengths to stay compatible with CPython's implementation details. A while ago I ran a toy program that generated its own bytecode on PyPy, to see what would happen, and to my surprise it just worked. I imagine that constrains them. V8 isn't bytecode-compatible with JavaScriptCore, at least to my knowledge.

The most pressing problems with Python's performance have more to do with implementation than with high-level language design.

PHP is the king of arbitrary, unconnected, bolted-on features, and it's pretty fast nowadays. Not much worse than Racket, eyeballing benchmarksgame, and sometimes better.

> and -- similar as C++ -- such an explosion in complexity?

I'm not so sure that it does. I'm given to understand the problem with C++ is that its features compose badly and interact in nasty ways. Do Python's? Looking at previously new features, I mainly see people complaining about f-strings and the walrus operator, but those are simple syntactic sugar that doesn't do anything crazy.

Instead of an explosion in complexity, I think there's merely a steady growth in size. People complain that it's becoming harder to keep the whole language in your head, and that outdated language features pile up. I think those are fair concerns. But these new features don't make the language slower (it was already slow), and they don't complicate other features with their mere existence.

The growth isn't even that fast. Take a look at https://docs.python.org/3.9/whatsnew/3.9.html . I wouldn't call it explosive.

I don't know enough C++ to program in it, but the existence of operator= seems like a special kind of hell that nothing in Python compares to.


> Even if you wanted to, it's hard to optimize when somewhere down the stack someone might call sys._getframe() and start poking at the variables twenty frames up. That's not quite a language design problem.

It's hard to optimize only if you accept the tenet that it sys._getframe(), and all its uses, must continue to work exactly the same in optimized code.

Instead, you can just declare that that it (and any related sort of anti-pattern of the same ilk) won't work in optimized code. If you want the speed from optimized compiling of some code, then do not do to those things in that particular code.

The programmer can also be given fine-grained tools over optimization, so as to be able to choose how much is done where, on at least a function-by-function basis, if not statement or expression.

It's not written in stone that compiled code must behave exactly as interpreted code in every last regard, or that optimized code must behave as unoptimized code, in every regard. They behave the same in those ways which are selected as requirements and documented, and that's it.

In C in a GNU environment, I suspect your Glibc backtrace() function won't work very well if the code is compiled with -fomit-frame-pointer.

In the abstract semantics of C++, there are situations where the existence of temporary objects is implied. These objects are of a programmer-defined class type and can have constructors and destructors with side-effects. Yet, C++ allows compete freedom in optimizing away temporary objects.

The compiler could help by diagnosing situations, as much as possible, when it's not able to preserve this kind of semantics. Like if a sys._getframe call is being compiled with optimizations that rule it out, a warning could be issued that it won't work, and the generated code for it could blow up at run-time, if stepped on.

One way in which compiled code in a dynamic language could differ from interpreted code (or less "vigorously" compiled code) is safety. For that, you want some fine-grained, explicit switch, which expresses "in this block of code it's okay to make certain unsafe assumptions about values and types". Then, he optimizer removes checks from the generated code, or chooses unsafe primitives from the VM instruction set.

The code will then behave differently under conditions where the assumptions are violated. The unoptimized code will gracefully detect the problems, whereas the vigorously compiled code will behave erratically.

This entire view can nicely take into account programmer skill levels. Advanced optimization simply isn't foisted onto programmers of all skill levels, so then they have to grapple with issues they don't understand, with impaired ability to debug. You make it opt-in. People debug their programs to maturity without it and then gradually introduce it in places that are identified as bottlenecks.


> In C in a GNU environment, I suspect your Glibc backtrace() function won't work very well if the code is compiled with -fomit-frame-pointer.

Actually, backtraces work correctly without explicit framepointers (in a typical GNU environment using ELF+DWARF).

The general concept has existed in DWARF since version 2 in 1992. The mechanism used for this is known as Call Frame Information (CFI)[0][1] — not to be confused with Control Flow Integrity, which is unrelated.

Here's some example libgcc code that evaluates CFI metadata[2]; there's similar logic in the libunwind component of llvm[3].

Burning a register on a frame pointer is a big deal on i386 and somewhat less so on amd64; there are other platforms where the impact is even lower. So, just know that you don't have to include FPs to be able to get stack traces.

If you're interested in how to apply these directives to hand-written assembler routines, there are some nice examples in [0].

[0]: https://www.imperialviolet.org/2017/01/18/cfi.html

[1]: https://sourceware.org/binutils/docs/as/CFI-directives.html

[2]: https://github.com/gcc-mirror/gcc/blob/master/libgcc/unwind-...

[3]: https://github.com/llvm/llvm-project/blob/main/libunwind/src...


> I don't think Rust suffers from the issues that make functional languages hard to compile, so that might be a bad example.

The issue is that in functional languages, the compiler has more information and can reliably rely on more assumptions, thus it can make more optimized code. This is why also Common Lisp can compile to very fast code (in a few of the cited micro-benchmarks, faster than Java).


Yes, a lot of these features are hard to integrate into Python because it has peculiar (to say the least) namespaces.

You just need to follow the mailing lists [1] and see that many core developers themselves are caught by some namespace abnormality.

It is a pity that Greenspun's 10th rule is in full effect again and we're stuck with an inferior Lisp/ML, especially in the scientific sector.

[1] Or rather the archives, since they are now de facto corporate owned. There is no free discussion any more and people are afraid to post.


> and we're stuck with an inferior Lisp/ML, especially in the scientific sector.

You will love Julia.

Here are some links:

https://julialang.org/blog/2012/02/why-we-created-julia/

Julia: Dynamism and Performance Reconciled by Design (https://dl.acm.org/doi/pdf/10.1145/3276490) <= This is great and surprisingly approachable.

https://opensourc.es/blog/basics-multiple-dispatch/

And when you start finding things that you miss, Julia and the community got you with excellent Metaprogramming support.

https://github.com/thautwarm/MLStyle.jl

https://github.com/MikeInnes/Lazy.jl

https://github.com/jkrumbiegel/Chain.jl


I know really little about Python except that it was heavily adopted by Google.

However for C++, I think that corporatization is having a strong negative influence on the language, which leads to it being stuffed with poorly integrated features which nobody really overlooks any more.



Are you on the wrong article discussion? Or is this just a personal axe to grind?


Honestly I'm not sure what all the fuss is about. I just skimmed over PEP 634 and PEP 636 (the accompanying tutorial) and I'm actually kind of excited to use this. I've missed this feature after using langs like Rust, Haskell, and Scala. People have pointed out some surprising edge cases such as this:

    NOT_FOUND = 404
    match get_status():
        case NOT_FOUND:
            # this actually assigns to the global NOT_FOUND
            # and is effectively the default case
        case _:
            # this never gets triggered
Yeah, I agree that's a bit ugly. But you can apparently do this:

    class Status(Enum):
        NOT_FOUND = 404

    match get_status():
        case Status.NOT_FOUND:
            # works as expected
        ...
And really how is any of that more ugly than this?

    def func_with_default(foo={}):
        if "bar" not in foo:
            # "bar" is set in the module-level dict that
            # was initialized when the func def was parsed,
            # not in a dict that is created per call
            foo["bar"] = ...
        ...
And we've been living with that one for years. These are just the things that you have to learn when you start using Python, just like any other language.

I, for one, am excited to start using this syntax.


A previous mistake does not make a convincing argument for making a new mistake. Also, the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Pattern matching is a desired feature, but the shortcomings are too costly for what it provides. You just outlined yourself the quickest foot-gun I've ever seen.

My second problem is that this violates many of the invariants you rely on while programming Python. f(x) always gives you the value of applying f to x. Nope, not in case. | means bitwise or. Nope, not in case. a means the value of a. NOPE. Not in case.


A variable name has never referred to the value of it in expressions where it is being assigned. In 'def f(a,b,c)' or '(a,*_) = x' the term 'a' doesn't refer to the value of a either. A match statement in this sense is just an assignment that has a graceful failing mechanism (rather than e.g. '(a,*_) = x' which throws for empty lists).

And I think the ship on '|' has sailed with the dictionaries.


> Also, the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Why would it need to copy anything? What's wrong with just evaluating the default argument expression every time the function is called with a default argument rather than once at definition-time? That's how it works in other languages.


The variables used in the expressions might not be in scope (in fact they usually aren't). Also, I'm rather sure that's how it works in C++ (which by accident copies arguments instead of leaving a reference, but the single time default argument evaluation holds).


> The variables used in the expressions might not be in scope (in fact they usually aren't).

That's solved easily enough by evaluating the expression in the scope where it was defined (again that's what other languages do).

> Also, I'm rather sure that's how it works in C++

Default arguments in C++ work as I described: If you define `void f(int arg = g(x)) {}`, then calling `f()` n times will call `g` n times as well (using the `x` that was in scope when `f` was defined) - and if you never call `f`, `g` isn't called either.

An example demonstrating this: https://ideone.com/vs26Oq


That’s a good idea, I like that!


Isn't it the same in ML though (using the same syntax in different contexts) ? Compare `| [a, b] => a + b` and `let my_list = [a, b]`. Same in JS, `let {a, b} = {a: 1, b: 2}` and `let d = {a, b}`. It's weird at first but then you just get used to it.

edit: updated list name since pipe ("|") and small L ("l") look kind similar in the code snippets.


> the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Why would it be a problem to do a copy in this case?

> f(x) always gives you the value of applying f to x

Not if it is preceded by "def"

> | means bitwise or.

Not if it had been overloaded, like many popular libraries do.

> a means the value of a

Not if it is followed by "="

How is that different from the new pattern matching syntax ?


> Why would it be a problem to do a copy in this case?

How are copy semantics even defined unambiguously?

> Not if it is preceded by "def"

In that case it is analogous, def f(x) DEFINES what f(x) means. For class definitions it's different, and it is indeed debatable if `class A(SuperClass)` is good syntax. I would have preferred `class A from SuperClass`, but that ship has sailed.

> | means bitwise or. Not if it had been overloaded, like many popular libraries do.

You missed the point willingly I think. See sibling comment. You cannot overload its meaning in a match, and the meaning is not related to what it otherwise means (bitwise or, set union, etc.).

> a means the value of a Not if it is followed by "="

And again, this is by analogy. a = 1 means that you substitute a with 1 wherever a occurs. Not so with the pattern matching syntax. If we had a = 1 then a == 1. Where is this analogy for pattern matching? How do you even make the comparison? You do not.

All of your counterpoints have reasonable analogies within the syntax itself (eg: the inverse of a function call is a function definition).


"f(x)" and "|" inside a regular expression mean something different. Similarly here, pattern matching has its own sub-language (a DSL).


Coincidentally, regular expressions are not part of the Python syntax. They're just string literals.


Are you being intentionally ironic here, or do you not know that regex DSL is written only inside of strings in python, not within the Python's syntax?


For what it's worth, I tried a modified version of the first example you've used above in the sandbox:

  NOT_FOUND = 404
  match get_status():
      case NOT_FOUND:
          print("Here")
          # this actually assigns to the global NOT_FOUND
          # and is effectively the default case
      case _:
          # this never gets triggered
          print("There")
I got a meaningful error:

    File "<ipython-input-13-5264d8327114>", line 3
      case NOT_FOUND:
         ^
  SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable


That in itself is weird though. Why throw a syntax error there but not here?:

    def f(x):
      return 1
      y = x + 1
      return y


You’ve braver than me, I had the same thoughts but didn’t dare post in here.

Not entirely surprised by the negative responses and I can see there are gotchas here, which is always thorny. But everyone seems to be picking simple uninteresting use cases so they can knock then down.

This feature would seem to be for cases where you need to do more complex matching.

In terms of the variable assignment, surely the whole point of this feature is to give you nice assignment based on matching, rather than using it as an if or switch statements?


> Yeah, I agree that's a bit ugly.

It's not about ugliness, it's about clarity and robustness. It's about not ingraining landmines into the language, whether beautiful or ugly. This is just asking to be blown up in people's faces over and over again.


The behaviour my intuition would have expected is that if a variable is currently in scope, then it is considered as a literal match, otherwise it's a capture match.

I suppose there's an obvious flaw to that behaviour, though.


No, that's not robust. The idea was simple - to use explicit marker like:

case == FOO: case is True:

That was part of PEP642, but it ended up containing more other questionable things. I hope that particular point will be separated and implemented.


module-level dict?... why...


Uh...that's my point. Everyone new to Python has to eventually learn not to do that.


It's a simple rule: Default arguments are evaluated at function definition time (when the interpreter reaches the 'def' statement) not at the time the function is called.

It's not a "module-level dict", you can create functions inside other functions:

    def foo():
        def bar(a={}):
            ...
        return bar
Each instance of bar gets its own dict when you run foo() and the "def bar(...): ..." statement gets executed. The dict is packaged up inside the function object (sort of like a closure except that the dict and it's name aren't from the enclosing scope of the function, and of course you can "shadow" it by passing in an argument for the parameter. https://en.wikipedia.org/wiki/Closure_(computer_programming) )


I understand this simple rule, and I understand closures, of course, it's just using closure semantics for default arguments doesn't make any sense, aside from some optimization maybe, and pretty dangerous. Other approaches are possible, see ruby, for example, default arguments are evaluated basically as if they're already in the body of running function, every time, only when they're not passed, and with the same lexical scope as the function/method. And what, python retained this behavior even in 3.x? omg.


Ah, sorry for over-explaining.


The folk over at openAI do something similar in order to create a registry, so that you can initialize objects using a name.


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`).


Yes, 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")


I dig it. 'as' primes my brain to something going into a scoped context.


It’s fine. Python just introduced expression assignment. Why not use that?

    match subject
        case [first := _, rest := _]
            return [rest, first]


That works too but I guess I am still getting used to the walrus operator. What really bugs me about this PEP is they are going through all this effort to introduce match but they don't have match assignments, which is personally one of my favorite things about match.


Well get to Perl eventually


Note that the original PEP622 relied on ":=" initially, but they found some corner case with it (apparently, operator precedence related) and switched to "as" in PEP634.


I'm still wary about this change making it in to Python, but I like this suggestion. It makes the assignment clear. The way it's currently specified would definitely trip me at some point.


I like your proposal. The `as` keyword is very readable to me.

I would have preferred `as` instead of the walrus operator too.


This would be really confusing to anyone who is used with match statements in other languages


For 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.


The 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.


Comparing garbage syntax to worse syntax and saying “it’s not as bad as that” isn’t the way one improves upon a language.


Which 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.


Almost everyone in this discussion is making exactly those comparisons. Including yourself. When you're discussing usability issues due to changes to the syntax, the perspective of non-exclusive developers vs full time Python devs doesn't change the underlying context of the discussion regarding the change in usability.

And I stand by my position that defending a bad decision because of the existence of worse decisions is a really piss poor counterargument.

Disclaimer: I'm a language designer myself so I know first hand how hard it is to get these things right.


>And I stand by my position that defending a bad decision because of the existence of worse decisions is a really piss poor counterargument.

This thread was just about the two alternatives (the PEP and explicit capture), not about the PEP in general, or about defending the PEP or even saying that the better alternative is "good". We just say it's better than the PEP. Not sure how you read that into what we wrote.

>Disclaimer: I'm a language designer myself so I know first hand how hard it is to get these things right.

Then go make that argument in some thread in this post discussing the PEP proposal in general?


> This thread was just about the two alternatives (the PEP and explicit capture), not about the PEP in general, or about defending the PEP or even saying that the better alternative is "good". We just say it's better than the PEP. Not sure how you read that into what we wrote.

Bullshit. You said, and I quote, "There are 100s of things in Python who would be confusing". That's what I was responding to. And my point stands: just because there are other pain points in Python doesn't mean we should accept more pain points into it.

> Then go make that argument in some thread in this post discussing the PEP proposal in general?

I’d prefer to make that argument in the thread where I was already talking you about the PEP proposal in general. ;)


But the existing semantics are very confusing to anyone who is used to Python, which seems like a bigger problem.


I don't agree, I'm used to Python and this is not confusing to me because I have learned to use match statements, you will do too.


I 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.


OK, 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.


Does it shadow the outside variable or mutate it?


Mutates it. Python `match` statements do not introduce a new scope.


Ouch, that's indeed pretty bad. I do expect `not_found = status` (that's how pattern matching works in several other languages), but it should be in its own scope, so that `not_found` is still `404` outside of the `match` block!


Python doesn't have block scope. This can lead to some surprising behaviour:

  x = -1
  for x in range(7):
      if x == 6:
          print(x, ': for x inside loop')
  print(x, ': x in outer')
This outputs:

  6 : for x inside loop
  6 : x in outer
For consistency, I think it makes sense for match to behave in a similar way.


It would make even more sense for Python not to adopt language features that can't be made to behave consistently with the language's existing syntax and semantics without introducing horrendous footguns.

I've been programming in dialects of ML for much longer than Python, so I absolutely appreciate the attraction of this feature. But, the opinion I'm coming to as I think more about this is:

  1. I *love* pattern matching.
  2. But not in Python.


Exactly. At this point it feels like Python is LARPing at being an ML. Without even block scoping, this is just asking for trouble.


To be fair, lots of languages have been moving closer to ML recently (and new languages tend to look more ML-like than in the past). That includes the adoption of pattern matching, but also things like value objects, first-class functions, sum types, named tuples, algebraic datatypes, type inference, etc.

I don't think that's a bad thing. I do think care should be taken when incorporating features from other languages, to see how it interacts with existing features, what the best form of that feature would be, and perhaps whether some different feature could achieve a similar goal.

(For the latter point, I find it unfortunate that languages which already contain 'try/catch' have been introducing 'yield' and 'async/await' as separate features; rather than generalising to 'shift/reset' and implementing all three as libraries)


Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


No, not interested in block-level scoping in Python. Why on earth would I ask a programming language I rely on for getting important work done to introduce so massive a breaking change as changing its scoping style?

"I don't think feature X is a good fit for Python because it interacts poorly with existing Python features Y and Z" is not a tacit statement of support for changing features Y and Z. It's a statement that means exactly what it says.

Like I alluded to in my grandparent post, I use other languages that are not Python, and like their features, too. That does not necessarily mean I want all my favorite features from those pulled into Python. Nor do I want my favorite features from Python pulled into other languages.

The best analogy I can think of is a college friend once developed this sort of transitive theory of food that went, "If A tastes good with B, and B tastes good with C, then A must taste good with C." This resulted in a number of questionable concoctions, like serving cottage cheese on top of chocolate cake because they both taste good with strawberries.


Actually, the opposite. Call me an old crank but I feel like Python should stay Python and not try to (badly) tack on ideas from ML.


For my part, as I maintain some complex academic research software, I would be much more interested in continuing support for Python 2, and more comprehensive numerical, math and vector libraries for Racket.


I haven't been programming in languages with pattern matching longer than Python and I still agree with you. Pattern matching is awesome, but it doesn't suit Python at all. I hope they reconsider adding it.


No, we'll rather fix issues people see with it.

Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


> It would make even more sense for Python not to adopt language features that can't be made to behave consistently with the language's existing syntax and semantics

Current match behavior is consistent with existing syntax and semantics.

And no, not adopting new features known to be generally useful across programming languages is not "better". Instead, better to continue elaborating the language even after the initial pattern matching support is added.

Interested in block-level scoping in Python? Please post on the python-ideas mailing list.


Why did you chop the last four words off of that sentence you quoted? All it accomplishes is making it so that the response rebuts a statement that wasn't actually made.


I don't know if I even know what "being consistent" would mean given the example here:

https://news.ycombinator.com/item?id=26082225

We don't shadow variables when they are re-used in comprehensions for example:

    >>> i = 4
    >>> [i for i in range(10)]
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    >>> i
    4
Given they're already accepting the oddity of having "case 404:" and "case variable:" mean very different things, I think they should have just gone the whole way and _not_ mutated outside variables. There seems to be little consistent with this addition to the language.


> We don't shadow variables when they are re-used in comprehensions for example:

I think "for example" makes this sound more general than it is. List comprehensions are one of the rare few constructs that introduce a new scope. The match block is more syntactically similar to if/while/etc. blocks, and so it makes sense that it would follow similar scoping rules.

This is not to say that I agree with the design as a whole. I think they should have went with:

  case obj.some_value:
      ...
  case some_value:  # behaves the same way as the previous
      ...
  case _ as variable:
      ...
  case (variable := _):  # an alternative to the previous syntax
      ...
I.e. I think the complete situation is messy, but it's not the variable scoping rules' fault.


> The match block is more syntactically similar to if/while/etc. blocks, and so it makes sense that it would follow similar scoping rules.

I would argue that a stack of 'case's are a 'lambda' (and in particular a single 'case x: ...' is equivalent), and hence they should have function scoping like 'lambda' does.

'match' is a distraction; it's just a function call written backwards, i.e. `match x: f` is equivalent to `f(x)` (in this case Python happens to require `f` to be implemented using the stack-of-'case's form, but that seems incidental; similar to how 'raise' and 'except' are a general form of control flow, despite Python requiring values to be wrapped in something descended from 'Exception')


> I think "for example" makes this sound more general than it is. List comprehensions are one of the rare few constructs that introduce a new scope.

The rarity of the construct doesn't matter honestly. Are you saying that list comprehensions should _not_ have introduced a new scope because it was unusual?

> The match block is more syntactically similar to if/while/etc. blocks, and so it makes sense that it would follow similar scoping rules.

Syntactically yes, but semantically it is very different and the semantics must be taken into account as well. The example given at the top of this thread (case 404: and case variable:) is enough to convince me that having variable scoping is a brain-dead obvious requirement.

> I think the complete situation is messy, but it's not the variable scoping rules' fault.

I agree with that statement. I think that improving the design/semantics would be more effective than just adding some more scoping rules in. In fact, I don't think this PEP should have been accepted in this current form at all. But given the current design, block-level scoping is appropriate. Given another design like maybe those you mention might not require that, but I think focusing on the fact that python doesn't have block-level scoping makes no sense. The python language before this PEP is not the same as the one after this PEP. The new one should have block-level scoping in this instance.


> I think they should have just gone the whole way and _not_ mutated outside variables. There seems to be little consistent with this addition to the language.

Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


In those other languages, if `not_found` is equal to 404 before the match block, how do you write a clause that only matches if `status` is 404?

Do you have to use the integer literal 404 instead of the named integer `not_found`?


Elixir uses the “pin operator”

“Use the pin operator ^ when you want to pattern match against a variable’s existing value rather than rebinding the variable.”

    iex> not_found = 404
    404
    iex> ^not_found = 200
    ** (MatchError) no match of right hand side value: 200
https://elixir-lang.org/getting-started/pattern-matching.htm...


Exactly, Elixir is usually the only example given. So, how it makes sense to blame Python here is unclear. There's a proposal to add similar "sigils" to Python pattern matching, it just didn't make it yet.


Depends on the language, but a lot of them use guard clauses (so case ... if cond =>).



In OCaml and F# you can use `when`:

    match status with
    | n when n = not_found -> ...


Reassignment not mutation


Ah yes, you’re right.


I don't get it, isn't that exactly what it's doing here?


It'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).


Reassigning not mutating


Reassigning to a variable is how you mutate a variable, isn't it?

The value being [im]mutable is a different topic entirely.


There aren't actually "variables" in Python in the sense of named values, instead there are namespaces where values are stored and looked up under string identifier keys. (Just to add spice, some values, namely function and class objects, do get their names embedded in them, but this is only used for debugging. They are stored in enclosing namespace dicts like other values.):

    >>> def f(): pass

    >>> g = f

    >>> g
    <function f at 0x000001B4A686E0D0>

    >>> locals()
    {'__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__doc__': None,
    '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
    '__name__': '__main__',
    '__package__': None,
    '__spec__': None,
    'f': <function f at 0x000001B4A686E0D0>,
    'g': <function f at 0x000001B4A686E0D0>}


Wait, what's your definition of variable?

Because what you're describing fits perfectly into what I would call a variable. There is a mapping from an identifier to a slot that can store a value (or reference), and that mapping is stored in a specific scope. I would call that mapping a variable.

I'm not sure exactly why you mentioned objects that have names embedded in them. Is that relevant to the definition you're using?


> Wait, what's your definition of variable?

With full formality, the definition of "variable" depends on context. In assembly language and C variables are names for storage locations in RAM, in Python they are name bindings in a dict data structure.

One distinction we could make is whether the names are compiled away or kept and used at runtime.

In any event, the important thing is to keep clear in one's mind the semantics of the language one is using. In Python you have values (objects of various types: ints, strings, tuples, lists, functions, classes, instances of classes, types, etc.) some of which are mutable and others are immutable, and you have namespaces: dicts that map strings to values. These namespaces have nothing to do with the location of the values in RAM.

So it doesn't really make sense in Python to speak of "mutate a variable", you can mutate (some) values, and rebind values to names (new or old).

> I'm not sure exactly why you mentioned objects that have names embedded in them. Is that relevant to the definition you're using?

Not really, it's just another little point that sometimes confuses some people when they are coming to grips with the idea that in Python values are not associated with names except by namespace bindings. There are some values (function and class objects) that do get associated with names.


I suppose, but it's very strange to say "Assigning to a variable is classified as mutating the variable in some languages but not others, even though the underlying mechanism works exactly the same way."

Shouldn't terms like this be cross-language?


The underlying mechanism doesn't work the same though, e.g. in C assigning to a variable "mutates" the contents of RAM, in Python it mutates the namespace dictionary (which of course also results in some RAM contents changing too, but through a relatively elaborate abstraction.)

> Shouldn't terms like this be cross-language?

Variables in C are different beasties than variables in Python, and variables in Prolog are different from both, and none of those are like variables in math. It's a source of confusion that we use the word "variable" for a host of similar concepts. The word "type" is similarly overloaded in CS. (See https://www.cs.kent.ac.uk/people/staff/srk21/research/papers... )

FWIW, I'm just explaining what wendyshu was on about above. :)


That's not exactly how C works, but I wasn't going to use C as my primary example, I was going to use Rust. You have to write "let mut" to let a variable be reassigned/mutated, and variables in Rust are almost identical to ones in Python as far as being an abstract 'namespace' binding.


> That's not exactly how C works

Sure, I elided a bajillion details, and who knows what the CPU is doing under the hood anyway? :)

I've never used rust (yet) so I can't really comment on that.

FWIW namespaces in Python aren't abstract, they exist as dicts at runtime. You can modify the dict you get back from locals() or globals() and change your "variables" in scope:

    >>> d = locals()
    >>> d['cats'] = 'meow'
    >>> cats
    'meow'


Reading the discussion, I think the criticism is more motivated by the subtle but crucial differences to most functional programming languages.


I was thinking more about the common criticism that something like `case Point2d(x, y):` "looks like an instantiation" and hence an equality check.

I actually replied to the wrong comment after reading several that visually looked similar at the time, so apologise for causing confusion in this subthread.


> EDIT: The lack of block scoped variable thing does seem like a wart right enough.

This is not specific to "match" statement, but is a general issue in Python. And thus, needs to have a general solution, orthogonal to pattern matching. Are you interested? Please post to the python-ideas mailing list.


Oh, 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"


The inconsistency just makes it worse and will lead to even more bugs. I foresee lint rules prohibiting match statements in the near future.


Couldn'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...


A 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}"


Yes, 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.


That'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.


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; 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)'.


>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?


Yes. For example:

    def neighbourhood(position):
      return map(
        lambda position: EDGE if position is None else position.absolute,
        position.neighbours
      )
The inner lambda is shadowing the name 'position'. This does two things:

1) It declares that the lambda doesn't depend on the argument of 'neighbourhood'

2) It prevents us referring to that argument by mistake

Compare it to a non-shadowing version:

    def neighbourhood(position):
      return map(
        lambda neighbour: EDGE if neighbour is None else position.absolute,
        position.neighbours
      )
Oops, I've accidentally written 'position.absolute' instead of 'neighbour.absolute'!

This version doesn't make any declaration like (1), so the computer can't help me find or fix the problem; it's a perfectly valid program. A static type checker like mypy wouldn't help me either, since 'position' and 'neighbour' are presumably both the same type.

It's not even clear to a human that there's anything wrong with this code. The problem would only arise during testing (we hope!), and the logic error would have to be manually narrowed-down to this function. Even if we correctly diagnose that the 'if' is returning a different variable than it was checking, the fix is still ambiguous. We could do this:

    EDGE if position is None else position.absolute
Or this:

    EDGE if neighbour is None else neighbour.absolute
Both are consistent, but only the second one matches the shadowing example.


> Oops, I've accidentally written 'position.absolute' instead of 'neighbour.absolute'!

I'm going to be honest here, the number of times I've made that kind of mistake is absolutely dwarfed by the number of times I have used the wrong variable because I had accidentally shadowed it.

Neither mistake is super common, but I can't recall ever writing 'position.absolute' instead of 'neighbour.absolute' unless I legitimately needed both position and neighbour in scope and the problem was hard to reason about. I can recall accidentally reusing a variable like 'x' as an iteration variable and then using the wrong 'x' because I forgot, and I can also recall misunderstanding what some piece of code did because I thought 'x' was referring to the outer scope but I had missed that it was shadowed by another declaration. Shadowing has caused me many more problems than it solved, at least in my own experience.


>Oops, I've accidentally written 'position.absolute' instead of 'neighbour.absolute'!

That's a contrived example though, if I ever saw one.

I don't think that's the kind of issue people commonly have, compared to misuse of shadowed variable further down the scope.

And for your example, a better solution would be for the close to declare what it wants to use from its environment. Python doesn't allow this syntax, but some languages do:

def neighbourhood(position): return map( lambda neighbour use (): EDGE if neighbour is None else position.absolute, position.neighbours )

Now the compiler can again warn you, since you're only allowed to use neighbour in the lambda.


I'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.


That's how structural pattern matching works in ML-like languages, where it's been in use for 40+ years.


Don't those languages typically have immutable variables so you would not be able to rebind a constant by accident?


This 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.


Python `match` statements do not introduce a new scope.

The second example does indeed change the value of `not_found`.


What'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.


It's consistent with how scoping works in loops and `with` clauses. Agreed that it's more problematic here, though.


That works the same as the rest of the language, doesn't it?

(That is, for, if, while, etc. also don't introduce new scopes.)


"Introducing a new scope" is not a concept that exists in Python, you only have function scope and module scope.


Having only function, module, class, generator, etc. scope in Python before this PEP might have made sense, but they really should have added pattern matching scope to keep things sane here.


There are some exceptions to this - for example, the iteration variable(s) in a list comprehension are only in scope within the comprehension.


Worth noting that this was _fixed_ in Python 3.

In Python 2.7 you get this:

    >>> my_list = [x for x in 'hello']
    >>> x
    'o'


Thanks, I didn't realize that. That makes their choice really baffling.


The 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.


and class scope


So you can easily create a variable in a match case


Oh dear, that is indeed very bad.


That's a very problematic design decision, imo...


Doesn’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.


Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


In Rust no


Apparently it's reassigning the existing not_found variable from the outer scope, not binding a new not_found variable in an inner scope.


This 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.


Not the scoping rule. For example, in Haskell:

    let x = foo
     in (
       case bar of
         x -> x,
       x
     )
This will give `(bar, foo)`: for the first element the `x` in the case will match against `bar` and return it, then that `x` will be discarded as we leave its scope; the second element uses the binding of `x` in the `let`.

According to the Python semantics we would get `(bar, bar)`, since we only have one `x` variable. When the case pattern succeeds, the existing `x` is updated to refer to `bar`. Hence we get the `bar` we expect in the first element, but we also get `bar` as the second element, since that `x` variable was updated. (Note that Python guarantees that tuple elements are evaluated from left to right).


That's just an inevitable consequence of the fact that, in Python, "scope" is synonymous with "dictionary attached to some object." This is already how for-loops work.


It's not "inevitable". They could have required local variables in case statements to be local to that case statement. It would have required changes to the "scope is synonymous with dictionary attached to some object" idea or maybe it would have required a dictionary to be attached to a case statement. I personally think local scope should have been viewed as a hard requirement if they were to introduce this to the language.


Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


I'm interested in sane semantics. In this case, that calls for block-level scoping. Those who introduced pattern matching should have understood that the lack of block-level scoping _before_ this PEP does in no way support the continuing of the status quo. The language after this PEP has changed and has turned into one where block-level scoping is appropriate in this case.

I'm honestly _not_ interested in block-level scoping in this case because I would _never_ have wanted this PEP to be accepted. This feature was quite controversial on the various python mailing lists, and yet the steering committee accepted it anyway. The steering committee might consider leading with a bit more humility and _not_ accepting such controversial PEPs. This is an example of language devolution and not evolution.


It occurs to me that there's a nice way to understand this from what's happened in Scala.

Scala has always had built-in syntax for pattern-matching, like:

    foo match {
      case bar => ...
      case baz => ...
    }
However, Scala also has a thing called `PartialFunction[InputType, OutputType]`, which is a function defined 'case by case' (it's "partial" because we're allowed to leave out some cases). This is essentially a re-usable set of cases, which we can apply to various values just like calling a function.

For example we can write:

    val f: PartialFunction[A, B] = {
      case bar => ...
      case baz => ...
    }

    f(foo)
Scala also allows us to attach extra methods to certain types of value, via 'implicit classes' (which were added late on in Scala's history, although similar patterns were available before). As of Scala 2.13, the standard library attaches a method called `pipe` to values of every type. The `pipe` method simply takes a function and applies it to this/self. For example:

    val f: PartialFunction[A, B] = {
      case bar => ...
      case baz => ...
    }

    foo.pipe(f)
However, now that we have these two things (`PartialFunction` and `pipe`), it turns out we don't need explicit syntax for `match` at all! We can always turn:

    foo match {
      case bar => ...
      case baz => ...
    }
Into:

    foo.pipe({
      case bar => ...
      case baz => ...
    })
Hence Scala, in a round-about way, has shown us that pattern-matching is essentially a function call.

When it comes to Python, it doesn't even need to be a discussion about block scope; it's equally valid to think of this as function scope (like Python already supports), where `case` acts like `lambda`, except we can define a single function as a combination of multiple `case`s (like in the Scala above).


As said many times already, then you have the opposite problem - how to get value from "inner" to "outer" scope. If we talk about function scope, then it requires "nonlocal" declaration in the inner scope. From Python, too many declaration like that are syntactic litter. It has a scoping discipline which allows to avoid them in most cases, and that works great in 90% of cases (popularity of Python and amount of code written in it is there proof).

Yes, there're still remaining 10%, and pattern matching kinda drew attention to those 10%. I'm interested to address those, and invite other interested parties to discuss/work together on that. The meeting place is python-ideas mailing list.


> If we talk about function scope

Note that I'm not simply saying 'match should have function scope', I'm saying that 'case' is literally a function definition. Hence functions defined using the 'case' keywork should work the same as functions defined using other keywords ('def', 'lambda' or 'class').

> you have the opposite problem - how to get value from "inner" to "outer" scope

The same way as if we defined the function using 'lambda' or 'def' or 'class'

> it requires "nonlocal" declaration in the inner scope

That's not a general solution, since it doesn't work in 'lambda'; although this exposes the existing problem that there is already a difference between functions defined using 'def'/'class' and functions defined using 'lambda'. Adding yet another way to define functions ('case') which defines functions that act in yet another different way just makes that worse.


> I'm saying that 'case' is literally a function definition

And I don't agree with saying it like that. I would agree with "a 'case' could be seen as a function definition". In other words, that's just one possible way to look at it, among others.

Note that from PoV of the functional programming, everything is a function. And block scope is actually recursively lexical lambda.

And OTOH function inlining is a baseline program transformation. Currently in Python, whether a syntactic element (not explicitly a function) gets implemented as a function is an implementation detail. For example, comprehension happen to be implemented as functions. But just as well they could be inlined.

Note that function calls are generally expensive, and even more so in Python. Thus, any optimizing Python implementation would inline whenever it makes sense (called once is obviously such a case). (CPython hardly can be called an optimizing impl, though since 3.8, there's noticeable work on that).


I mentioned that in other comments, and can repeat again, there were 2 choices: a) add initial pattern matching to reference Python implementation; b) throw all the work into /dev/null and get back to dark ages where pattern matching is implemented in hacky ways by disparate libs and macros. Common sense won, and a) was chosen. Pattern matching will be definitely elaborated further.

> This feature was quite controversial on the various python mailing lists

I'm also on various Python lists, and what I saw that various details were controversial, not pattern matching itself. Mostly, people wanted pattern matching to be better right from the start, just like many people here. Well, I also want Linux version 234536464576.3.1-final-forever, but instead run 5.4.0 currently, and install new versions from time to time. The same is essentially with Python too.


> throw all the work into /dev/null and get back to dark ages where pattern matching is implemented in hacky ways by disparate libs and macros.

How does not accepting this PEP throw anything away? It's literally right there. It's still hosted there on the PEP site. Those who want pattern matching can continue to refine the work. "Common sense" requires understanding the current work is a sunk cost and in no way supports its introduction into the language.

> I'm also on various Python lists, and what I saw that various details were controversial, not pattern matching itself.

The details of the PEP are the problem, not the idea. Not accepting this PEP is not the same as rejecting pattern matching. This is only one possible implementation of pattern matching. It's also a bad one and one that makes the language worse. Rejecting this PEP allows a better implementation in the future.


> It's still hosted there on the PEP site.

On the PEP site, https://www.python.org/dev/peps/ , there're a lot of deadlocked PEPs, some of them a good and better would have been within, than without.

> Rejecting this PEP allows a better implementation in the future.

Let's count - 3rd-party patmatching libs for Python exists for 10-15 years. And only now some of those people who did their work as "third parties" came to do it inside mainstream Python.

The "future" you talk about is on the order of a decade. (Decade(s) is for example a timespan between 1st attempts to add string interpolation and f-strings landing).

I myself was ardent critic of PEP622/PEP634. I find situation with requiring "case Cls.CONST:" to match against constants to be unacceptable. But I'm pragmatic guy, and had to agree that it can be resolved later. The core pattern matching support added isn't bad at all. Could have been better. Best is the enemy of good.


> On the PEP site, https://www.python.org/dev/peps/ , there're a lot of deadlocked PEPs, some of them a good and better would have been within, than without.

If it's deadlocked, it really _shouldn't_ be added.

> Let's count - 3rd-party patmatching libs for Python exists for 10-15 years. And only now some of those people who did their work as "third parties" came to do it inside mainstream Python.

What's wrong with multiple implementations? Maybe people want different things? Besides the implementations' existence shows that lack of language support isn't something that blocks the use of pattern matching. Also moving it into the language doesn't mean people will work on that one implementation. Haven't you heard that packages go to the standard library to die? Why would it be any different in the python language. Besides I'm sure that the 3rd party libs will continue to be used anyway.

> But I'm pragmatic guy, and had to agree that it can be resolved later. The core pattern matching support added isn't bad at all. Could have been better. Best is the enemy of good.

I'm pragmatic too. I understand that I can do everything that this PEP introduces without the change to the language. I also understand that this PEP could continue to be worked on and improved. It's true that best is the enemy of good. I (and obviously many others here) believe that this is _bad_.


> What's wrong with multiple implementations?

It's absolutely great, and I'm saying that as someone working 5+ years on an alternative Python dialect (exactly with a motto of "down with toxic lead-acid batteries").

> Also moving it into the language doesn't mean people will work on that one implementation.

Only on that one - god forbid. But gather around that particular implementation to make it better and polish rough edges - for sure. (While the rest of impls will remain niche projects unfortunately.)

> I (and obviously many others here) believe that this is _bad_.

And me, and many others, believe it's good ;).


Well I guess the most useful information I've gotten out of this thread is that there are many other implementations already. I'll try to remember that the next time I see someone use the PEP version in one of my python projects so I can recommend them to use one of the third-party libs. I see no reason to believe they'd be any worse than this.


The fact that you weren't even aware that 3rd-party pattern matching solutions for Python existed before, makes me hard to believe that will put your actions where your words are. Mere searching on Github would gives 156 hits: https://github.com/search?q=python+pattern+matching . Divided by 2 for mis-matches, it's still sizable number of projects.

And that's problem #1 - you'll have hard time to choose among them (even though there're projects with 3.3K stars; but that of course doesn't mean such a project is the "best"). And secondly, many of them are indeed "worse" in the sense they're less general than the PEP version. Third common problem is sucky syntax - unsucky one require macro-like pre-processing of the source, and sadly, that's not a common norm among Python users (it should be, just as the availability of the block scope). I bet you will chicken out on the 3rd point, if not on first 2 ;-).

So yes, "official" support for pattern matching was in the dire need to organize the space. Now, 3rd-party libs can clearly advertise themselves as "We're like official patmatching, but fix the wart X/Y/Z". Bliss.


> The fact that you weren't even aware that 3rd-party pattern matching solutions for Python existed before, makes me hard to believe that will put your actions where your words are.

Well of course I won't use it myself. I don't find it necessary in python. My simple policy will be stand against any usage of this language feature in any code I write or contribute to. Those who want to use cases can either use other language features or third-party libraries which I'd have to study as well. Are you seriously looking down upon me because I haven't used third-party libraries that I consider unnecessary?

> And that's problem #1 - you'll have hard time to choose among them

This point is nonsense. All this shows is there is no agreement on how a third-party package should implement this feature. If anything, it argues against its inclusion in the language.

> And secondly, many of them are indeed "worse" in the sense they're less general than the PEP version.

All this says is that the PEP version isn't the worst implementation out there. It in no way implies that it should be included in the language.

> Third common problem is sucky syntax

So far this is the only time in all your posts in this thread that I've seen you give one reasonable argument. Congrats it took you long enough. So I'll give you this. Make the semantics non-idiotic (i.e. at least fix scoping as well as don't treat variable names and constants differently) and I'll accept it. I'm personally not against pattern-matching. I don't consider necessary by any stretch, but if its design makes sense it is at worst benign.

> So yes, "official" support for pattern matching was in the dire need to organize the space.

It's funny how the vast majority of feedback I see on the internet argues otherwise. It seems pretty clear this was neither needed not implemented well.

Anyway I'll bow out here. You seem less interested in learning what people outside of python-list actually care about or want and more interested in explaining why python-list's position is right. It requires impressive lack of self-reflection. Anyway pattern matching is in. The current form will make python a little worse as a language, but it's still overall quite good language. Maybe improvements will be made to make it tolerable (though I doubt it if your attitude is representative of python-list/python-dev/etc.). If stupidity like this keeps up the language will just slowly devolve, but it's not likely to be a bad language for many many years yet and well there are always other languages to choose from. It's unreasonable to expect a group to make good decisions forever.


> My simple policy will be stand against any usage of this language feature in any code I write or contribute to.

Dude, you're just like me! I have the same attitude towards f-strings ;-). Except I know that I will use them sooner or later. But I'm not in hurry. You maybe won't believe, but I found a use even for ":=" operator.

> So far this is the only time in all your posts in this thread that I've seen you give one reasonable argument.

Oh, you're so kind to me!

> You seem less interested in learning what people outside of python-list actually care about or want and more interested in explaining why python-list's position is right.

I'm a flexible guy. On Python lists, I'm argue against f-strings, assignment operators, and about deficiencies in proposed pattern matching. On interwebs with guys like you, I'm arguing trying to help them see the other side. And no worries, your opinion is very important to me.


> Let's count - 3rd-party patmatching libs for Python exists for 10-15 years. And only now some of those people who did their work as "third parties" came to do it inside mainstream Python.

Well, somewhat tongue-in-cheek, why not introduce a macro system into Python which allows to experimentally implement such syntactic changes as a library?


First of all, macro systems for Python exist for decades (just as long as pattern matching, and indeed, many patmatching implementations are done as macros). One well-know example of both is https://macropy3.readthedocs.io/en/latest/pattern.html

Secondly, there's a PEP to embrace macros in CPython (instead of pretending they don't exist, and leaving that to external libraries): https://www.python.org/dev/peps/pep-0638/

But the point, you don't need to wait for official PEP to use macros in Python. If you wanted, you could do that yesterday (== decades ago). And I guess in absolute numbers, the same amount of people use macros in Python as in Scheme. It's just in relative figures, it's quite different, given that there're millions of Python users.


So, if it isn't finished, why do people not use pattern matching implemented as a macro, until things like scoping are ironed out?


For as long as you're a human and belong to category of "people", you can answer that question as good as anyone else. And your answer is ...?

(Just in case my answer is: https://github.com/pfalcon/python-imphook , yet another (but this time unsucky, I swear!) module which allows people to implement macros (among other things)).


> Well, I also want Linux version 234536464576.3.1-final-forever, but instead run 5.4.0 currently, and install new versions from time to time. The same is essentially with Python too.

Just one thing, if mainline Linux would work like Python in respect to stability of APIs and features, you could start to debug and re-write your system after minor kernel upgrades. Linux does not break APIs, and this is possible because people are very careful what they implement - they will need to support it for an indefinite future.

Of course you can make patched branches and experimental releases of the kernel, these exist, but few people will use them, for good reasons.


The Linux kernel has a document imaginatively called "stable API nonsense": https://www.kernel.org/doc/Documentation/process/stable-api-...

But the talk was not about that, it was about the fact that we want to get "finished software", but as soon as we ourselves deliver software, we vice-versa want to do it step by step, over long period of time. One day, we should get some reflection and self-awareness and understand that other programmers are exactly like ourselves - can't deliver everything at once.


What you cite appears misleading to me - the text by Greg Kroah-Hartman talks very clearly about interfaces within the Linux kernel, not interfaces between kernel and user space, such as the syscall interface, which are stable. If you want to read the position of the lead Linux kernel developer on breaking user space APIs, here it is, in all caps:

https://linuxreviews.org/WE_DO_NOT_BREAK_USERSPACE

And here is the rationale why:

https://unix.stackexchange.com/questions/235335/why-is-there...

- you see that it makes perfect sense for a programming language like Python, too, to make only backwards-compatible changes (except perhaps if there are severe problems with a release).

In the same way, it does not matter how things are implemented within Python, but it matters a lot that the user interfaces, which includes in this case the syntax of the language, are stable.

And the fact that Python contrary to that does break backward compatibility - sometimes even in minor releases -, and continues to do so, is a reason that for my own projects I have come to the point at avoiding python for new stuff. There are other languages which are more stable and give the same flexibility, even at better runtime performance.


> for my own projects I have come to the point at avoiding python for new stuff.

But who are you, do I know you? I know some guy who said that about Python and now develops his own Python-like language. Is that you? Because if you just consumer of existing languages, it's something different, there always will be a new shiny thingy around the corner to lure you.

> There are other languages which are more stable and give the same flexibility, even at better runtime performance.

Yes, but from bird's eye view, all languages are the same, and differences only emphasize similarities. So, in a contrarian move, I decided to stay with Python, and work on adding missing things to it. Because any language has missing things, and Python isn't bad base to start from at all.


> you see that it makes perfect sense for a programming language like Python, too, to make only backwards-compatible changes

That's exactly what Python does of course (except when purposely otherwise, like 2->3 transition). And of course, that policy is implemented by humans, which are known to err.


> That's just an inevitable consequence of the fact that, in Python, "scope" is synonymous with "dictionary attached to some object."

What object is the scope of a comprehension (in Py 3; in py 2 they don't have their own scope) a dict attached to? And, if you can answer that why could there not be such an object for a pattern match expression?


> What object is the scope of a comprehension (in Py 3; in py 2 they don't have their own scope) a dict attached to?

The generator object that gets created behind the scenes.

> And, if you can answer that why could there not be such an object for a pattern match expression?

There could be, I suppose, just as there could be for "if" or "for". If Python decided to have lexical scoping everywhere, I would be in favor of that (but then people would complain about breaking changes). In lieu of that, I like the consistency.


>The generator object that gets created behind the scenes.

So like a match object that could get crreated behind the scenes?


If you have automatic block-level scoping, then you have the opposite problem - you need to do extra leg-work to communicate a value to the surrounding scope.

Anyway, anyone agrees that block-level scoping is useful. Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


>If you have automatic block-level scoping, then you have the opposite problem - you need to do extra leg-work to communicate a value to the surrounding scope.

In the general case, you just declare the variable in the surrounding scope and then affect it in the lower one, no?


Right. But the whole idea of Python scoping rules was to not burden people with the need to declare variables (instead, if you define one, it's accessible everywhere in the function).

But yes, block-level scoping (as in C for example) would be useful too in addition to existing whole-function scoping discipline.

Again, I'm looking for similarly-minded people to move this idea forward. If interested, please find me on the python-ideas mailing list for discussing details.


> then you have the opposite problem - you need to do extra leg-work to communicate a value to the surrounding scope.

that would be far less likely to break things in an unexpected way, as in "explicit is better than implicit".

I am also wondering whether what is really missing here is perhaps a kind of imperative switch/case statement which has the explicit purpose of changing function variables.


List comprehensions were changed in Python 3 to avoid this, so I don't think it's quite that simple.


In Python 3, list comprehensions use generators behind the scenes.


Well, this "inevitable" consequence also infects pattern matching.


> This is how pattern matching works in any language

No, its not, but no language (at least that I am aware of) except python does pattern matching + local variables + not introducing a new scope with the pattern match.

Ruby is the closest, but it does introduce a new scope while providing a mechanism for binding variables in the containing local scope. (As well as a method to “pin” variables from the containing scope to use them in matches.)


Not introducing a new scope with a match is unfortunate, but it's also consistent with how every other language feature interacts with scoping.

> (As well as a method to “pin” variables from the containing scope to use them in matches.)

This is a good idea, I agree -- at least for Python, where you would obviously just call __eq__.

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.


> Not introducing a new scope with a match is unfortunate, but it's also consistent with how every other language feature interacts with scoping.

Except comprehensions, which changed in Py 3 to have their own scope, rather than binding control variables in the surrounding (function or module) scope as in Py 2.

> 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.

It would be except:

* You can't access function-scoped identifiers that way.

* You can't access module-scoped identifiers in the main module that way.

* You can't conveniently reference identifiers in the current module that way. (I think you can use the full module path to qualify the name in the local module, but that's both awkward and brittle to refactoring, and there's pretty much never a reason to do it for any other purpose.)


Interested in block-level scoping in Python? Please post on the python-ideas mailing list. Thanks.


>Many of the comments here reveal a bizarre parochialism.

This seems like a bizarre misunderstanding of Python scoping rules, and how this can be a problem here.


In 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?


It will be to stick your constants in a module and use module.constant to match them. Or use an enum. Or hang them off a class.


Yes, of course. Again, this is how it works in basically every mainstream language with this feature.


Really? 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.


If 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.


No 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.


There are PatternSynonyms in Haskell.


Are 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.


It doesn't reassign the 'status' variable, it reassigns the 'not_found" variable.


> 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.


Why is it performing an implicit assignment instead of an equality check?


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. 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-...

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.


> 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.


That looks pretty counter intuitive even after this explanation


I 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


I 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.


According to the specification pointed to by choeger in this comment 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.


Thus, will be fixed sooner or later. (Sad it's not fixed right away, but neither me nor you proposed patches.)


Assignments going wrong is par for the course in python! I still am surprised it is taught to beginners...


That's nuts. Sorry, but ... that's nuts.


I will be waiting until pylint detects these sorts of footguns...


I just read the part about the name binding and its ... totally bonkers. Sorry, but I have no other words for this:

> The implementation may choose to either make persistent bindings for those partial matches or not. User code including a match statement should not rely on the bindings being made for a failed match, but also shouldn't assume that variables are unchanged by a failed match. This part of the behavior is left intentionally unspecified so different implementations can add optimizations, and to prevent introducing semantic restrictions that could limit the extensibility of this feature.

That's basically dynamic scoping instead of lexical scoping only inside the match blocks and only for pattern variables. Who in their right mind comes up with a design like that? That's the javascript route! I get it that proper binding would have been difficult but if language design is too difficult, you should stop and not go one like you don't care about the consequences! This decision will waste thousands of developer hours when searching bugs!


> This part of the behavior is left intentionally unspecified so different implementations can add optimizations, and to prevent introducing semantic restrictions that could limit the extensibility of this feature.

Isn't this more or less what was the argument to allow more undefined behaviour in ANSI C ?

(Edit: I think this also comes from the tension caused by integrating features from functional languages into an imperative language, like explained in more detail here: https://news.ycombinator.com/item?id=26086863 )


Unlike Python, C has the excuse of runtime efficiency as a plausible rationale.

Python is big time SLOW, there's a ton of untapped optimisation potential before wandering into UB for sake of speed territory.


They went for "UB" exactly because they understand the current behavior is not ideal, and to allow for better implementation and to change it later.


No worries, this was debated fiercely. In the end, we had to choose between "Python has a reference syntax for pattern matching" vs "Python continues to have disparate 3rd-party extensions for pattern matching, without clear leader".

The essence of the issue you quote is that Python needs block-level scope for variables (in addition to the standard function-level scope). Similar to what JavaScript acquired with "let" and "const" keywords. That's more pervasive issue than just "match" statement of pattern matching, and thus better handled orthogonally to it.


> The essence of the issue you quote is that Python needs block-level scope for variables (in addition to the standard function-level scope).

No, it's not. I mean, sure, either general or special-case (like comprehensions already provide, and there is no good reason for matches not to) isolated scope would solve this, but so would bind-on-success-only with existing scoping.


> The essence of the issue you quote is that Python needs block-level scope for variables

In this case, would it not better to fix block scoping first, and introduce the pattern matching feature later?

Also, this is by far not a easily agreeable issue because changing this will break a lot of ugly but working imperative code. This has easily more impact than changing the default string type to Unicode.

For me, adding pattern matching before defining scoping feels like a technical debt issue in a large code base - if you make a change that depends on an improvement, it is much cleaner to make that improvement first.

Also, Python has only one widely-used implementation, and arguably it will be difficult to change behavior of that implementation if people did came to rely on a certain scoping behavior.


> That's basically dynamic scoping instead of lexical scoping

No, it's not a scoping change, the variable in the pattern is definitively in the scope (function or module) containing the pattern match.

It's just non-determinism (or, no specification) as to what, if anything, gets assigned to it as a result of the failed pattern match.


It is. Normally, a match expression can bind variables to values. In lexical scoping, these variables exist inside the match expression. In dynamic scoping they exist outside. The design mistake seems to be to assume that the patterns inside a match expression should be regular expressions, whereas pattern expressions should be a dedicated syntactic category.


Python has lexical scoping in which modules, functions, and comprehensions are the only lexical units that introduce a new scope. The addition of pattern matching, and the UB around whether binding happens for failed matches, do not change that to dynamic scope, in which the course of execution, not the lexical structure, would determine visibility (e.g., variables would be visible in a function called from, even if not nested within, the one in which they were defined.)


guh.

another pylint rule to forbid using same names outside match clauses and inside.


You probably should do that anyway even if the scoping was local.


Funny how they accept this craziness but for the walrus operator there was endless debate

Bike shedding at its worse


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: