Maybe time to rethink? https://www.techrepublic.com/article/programming-languages-w...
If this is as promising as it sounds, it seems Python 4 now has its "thing" and is on the horizon. Or at least may become a serious thing to talk about
Maybe time to rethink? https://www.techrepublic.com/article/programming-languages-w...
If this is as promising as it sounds, it seems Python 4 now has its "thing" and is on the horizon. Or at least may become a serious thing to talk about
What struck me as most significant was the opportunistic breakage of things not related to the unicode transition. In the many years it took to win people over to v3, they could have marched over all the breaking changes a year at a time. Given that side-by-side installs of python3.x point versions are very functional, with or without venvs, this would have been much more palatable. Perhaps harder than it sounds though.
I attempted a couple of 2to3 translations of open source libraries over the years, with varying degrees of success. Every time I found that most of the changes were easy, but debugging the broken bits was hard due to the sheer volume of source changes. If instead I could have done conversions where there was only a single major semantic change at a time, it would be so much easier to figure out what was going wrong at any given step. Furthermore, I imagine that a single-breaking-change mentality would lead to better documentation on how to transition for each version.
For this reason, I have become rather suspicious of yearly release schedules. Swift is even more frustrating: the version changes are really just dictated by Apple's yearly PR calendar. Some big things get rushed out for WWDC before they are ready, and smaller fixes can get held back until the next year. I would much rather that the language teams just prioritize one thing at a time, release it when it is ready, and foster a community where staying up-to-date on the latest version is easy and desirable (a more complicated story for Apple than for Python I think, due to ABI, OS version, etc).
From past discussions on HN I've gathered that there is such a thing as release fatigue, where developers get irritated when libraries release breaking changes too often. Nevertheless I often wonder if languages and libraries could improve faster by making more breaking changes, one at a time, with robust side-by-side installs to facilitate testing across versions. I wish side-by-side library versions were possible in Python, just to facilitate regression testing.
Bringing this all back to the post, I sincerely hope that if Python 4 is a breaking change to the GIL, that it will be only that.
I'm curious what others think about all this. Thoughts?
That was the point of the "from __future__" imports. You could get most of the way toward Python 3 so that 2to3 would be easier to work with and the new semantics could be gradually baked into the code prior to migration.
Python 3 had 25 years of cruft to clean up. They won't have to do that again.
This.
It makes absolutely no sense to claim that having to deal with a single non-backwards compatible release is somehow worse than having to deal with a sequence of non-backwards compatible releases.
Even though the migration from Python2 to Python3 faced some resistence, if anything the decision was totally vindicated.
Corporate developers, who have taken over Python and other people's work, like unnecessary changes, because they get many billable hours of seemingly complex work that can be done on autopilot.
Corporations might even take over more C extensions whose developers are no longer willing to put up with the churn and who have moved to C++ or Java.
In the long run, this is bad for Python. But many developers want to milk the snake until their retirement and don't care what happens afterwards.
In my 20+ year career I have never worked with a programmer that matches this description.
Maybe I've got lucky.
I want the python that Guido promised me, with 2021 performance. I don't want some abhorrent committee-designed piece of middle-of-the-road shitware glue language that I must use because everyone uses it.
I want a language that doesn't spin it's single-threaded wheel in a sea of CPU cores, and I want a language that has one obvious way of doing things without needing to grok and parse dumb """clever""" hacks that will only be abused by midlevel programmers to show off hoe they saved typing a few lines of additional code.
To me, speed + simplicity = ergonomy = joy. I want a new python 4 to focus exclusively and intensely on performance improvements and ergonomy.
Gone are the days when you invest in a platform like python, and they make crazy decisions that kill the platform's future (e.g. perl5). Ignore small syntax stuff like := and focus on the big stuff.
That says nothing about their quality. It just says you like them. If you gave me unhealthy food I'd probably eat it immediately too. Doesn't mean I think it's good for me.
> Ignore small syntax stuff like := and focus on the big stuff.
They're not "small" when you immediately start using them in a "large fraction of your code". And a simple syntax that's easy to understand is practically Python's raison d'être. They added constructs with some pretty darn unexpected meanings into what was supposed to be an accessible language, and you want people to ignore them? I would ignore them in a language like C++ (heck, I would ignore syntax complications in C++ to a large degree), but ignoring features that make Python harder to read? To me that's like putting performance-killing features in C++ and asking people to ignore them. It's not that I can't ignore them—it's that that's not the point.
my_match = regex.match(foo)
if my_match:
return my_match.groups()
# continues with the now useless my_match in scope
Versus if my_match := regex.match(foo):
return my_match.groups()
# continues without useless my_match in scope
How is the second one less readable? Have you ever heard of a real world example of a beginner or literally anyone ever actually expressing confusion over this?The more major problem with the walrus operator is more complicated expressions they made legal with it. Like, could you explain to me why making these legal was a good thing?
def foo()
return ...
def bar():
yield ...
while foo() or (w := bar()) < 10:
# w is in-scope here, but possibly nonexistent!
# Even in C++ it would at least *exist*!
print(w)
# The variable is still in-scope here, and still *nonexistent*
# Ditto as above, but even worse outside the loop
print(w := w + 1)
If they just wanted your use case, they could've made only expressions of the form 'if var := val' legal, and maybe the same with 'while', not full-blown assignments in arbitrary expressions, which they had (very wisely) prohibited for decades for the sake of readability. And they would've scoped the variable to the 'if', not made it accessible after the conditional. But nope, they went ahead and just did what '=' does in any language, and to add insult to injury, they didn't even keep the existing syntax when it has exactly the same meaning. And it's not like they even added += and -= and all those along with it (or +:= and -:= because apparently that's their taste) to make it more useful in that direction, if they really felt in-expression assignments were useful, so it's not like you get those benefits either.I am baffled to learn that it's kept in scope outside of the statement it's assigned, and I agree it would have a negative impact on readability if used outside of the if statement.
No, I'm pretty sure that's intentional. You want the left-hand side of an assignment to be crystal clear, which "foo() or w := bar()" is not. It looks like it's assigning to (foo() or w).
def thing(): return True
if thing() or w:= "ok": # SyntaxError: cannot use assignment expressions with operator
pass
print(w)
. . .
if thing() or (w := "ok"):
pass
print(w) # NameError: name 'w' is not defined
The first error makes me think your concern (that w is conditionally undefined) was anticipated and supposed to be guarded against with the SyntaxError. I believe the fact you can bypass it with parentheses is a bug and not an intentional design decision.> The motivation for this special case is twofold. First, it allows us to conveniently capture a "witness" for an any() expression, or a counterexample for all(), for example:
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
I have a hard time believing even the authors (let alone you) could tell me with a straight face that that's easy to read. If they really believe that, I... have questions about their experiences.The beauty of Python...
From the PEP:
> An assignment expression does not introduce a new scope. In most cases the scope in which the target will be bound is self-explanatory: it is the current scope. If this scope contains a nonlocal or global declaration for the target, the assignment expression honors that. A lambda (being an explicit, if anonymous, function definition) counts as a scope for this purpose.
I find this particularly strange and inconsistent:
lines = ["1"]
[(comment := line).startswith('#') for line in lines]
print(comment) # 1
[x for x in range(3)]
print(x) # NameError: name 'x' is not definedI'm pretty sure it's what I explained here: https://news.ycombinator.com/item?id=28899404
False or w := 1:
Is grouped like so: (False or w) := 1
Which is a SyntaxError. That's... not a smart place for it to be in the operator hierarchy. I expected it to be near the very top, like await.1. https://docs.python.org/3/reference/expressions.html#operato...
Edit: 20 minutes later, can't respond.
There are two aspects I have been thinking about while looking at this: Introduction of non-obvious behavior (foot-guns) and readability. Readability is important, but I have been thinking primarily about the foot-gun bits, and you have been emphasizing the readability bits. I can't really accurately assess readability of something until I encounter it in the wild.
x := 1 if cond else 2
never resulting in x := 2 which is pretty unintuitive.And you have to realize, even if the precedence works out, nobody is going to remember the full ordering for every language they use. People mostly remember a partial order that they're comfortable with, and the rest they either avoid or look up as needed. Like in C++, I couldn't tell you exactly how (a << b = x ? c : d) groups (though I could make an educated guess), and I don't have any interest in remembering it either.
Ultimately, this isn't about the actual precedence. Even if the precedence was magically "right", it's about readability. It's just not readable to assign to a compound expression, even if the language has perfect precedence.
Here's another way to trigger the same NameError, via "global":
import random
def foo():
return random.randrange(2)
def bar():
global w
w = return random.randrange(20)
return w
while foo() or (bar() < 10):
print(w)
For even more Python-is-not-C++-fun: import re
def parse_str(s):
def m(pattern): # I <3 Perl!
nonlocal _
_ = re.match(pattern, s)
return _ is not None
if m("Name: (.*)$"):
return ("name", _[1])
if m("State: (..) City: (.*)$"):
return ("city", (_[2], _[1]))
if m(r"ZIP: (\d{5})(-(\d{4}))?$"):
return ("zip", _[1] + (_[2] if _[2] else ""))
return ("Unknown", s)
del _ # Remove this line and the function isn't valid Python(!)
for line in (
"Name: Ernest Hemingway",
"State: FL City: Key West",
"ZIP: 33040",
):
print(parse_str(line)) # w is in-scope here, but possibly nonexistent!
# Even in C++ it would at least *exist*!
because I don't see how bringing up C++'s semantics is relevant when Python has long raised an UnboundLocalError for similar circumstances.If I understand you correctly, you believe Python should have introduced scoping so the "w" would be valid only in the if, elif, and else clauses, and not after the 'if' ends.
This would be similar to how the error object works in the 'except' clause:
>>> try:
... 1/0
... except Exception as err:
... err = "Hello"
...
>>> err
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'err' is not defined
If so, I do not have the experience or insight to say anything meaningful.Python’s `if` statements do not introduce a new scope.
I will readily admit that the walrus operator doesn't do what I thought it did and I have no interest in whatever utility it provides as it exists today.
Definitely. You would think if they're going to undermine decades of their own philosophy, they would instead introduce variable declarations and actually help mitigate some bugs in the process.
if (percentage := ((student["marks"]/700)*100)) > 70:
As a non-Python programmer it is usually pretty easy for me to correctly guess what a piece of Python code does. (And once in a while I need to take a look at some Python code).
Walrus operator got me. I tried to guess what it did, but even having simple code examples I could not. My guesses were along the lines of binding versus plain assignment, or some such. None of my guesses were even close. I had to google it to find out (of course I could also read the documentation).
class C(object):
A = 1
B = 2
x = 3
y = 10
print(x - B)
match y:
case C.A:
print('A')
case B:
print(y - B)And no, this is not really a scoping issue. Match is literally writing to a variable in one pattern but not the other. A conditional write is just a plain inconsistency.
The sad part is both of these features are stumbling over the fact that Python doesn't have variable declarations/initialization. If they'd only introduced a different syntax for initializations, both of these could have been much clearer.
I guess I'm not sure where "design" ends and "implementation" begins? To me, how to handle matching on variables that already exists is both, because "pattern matching and destructuring" are the features and how that must work in the context of the actual language is "implementation". It being written in a design doc and having real world consequences in the resulting code doesn't make it not part of the implementation.
Instead of quibbling over terms, I was much more interested in whether you like the idea of pattern matching.
I think not liking the final form a feature takes in the language is fundamentally different from wholesale disliking the direction the language design is going.
But I mean, you can call it that if you prefer. It's just as terrible and inexcusable regardless of its name. And yes, as I mentioned, I would have loved to have a good pattern matching system, but so far the "direction" they're going is actively damaging the language by introducing more pitfalls instead of fixing the existing ones (scopes, declarations, etc.). Just because pattern matching in the abstract could be a feature, that doesn't mean they're going in a good direction by implementing it in a broken way.
I guess like they say, the road to hell is paved with good intentions.
By this definition, bugs and other unintended consequences that the user encounters are "Design".
> Calling that an implementation detail is like calling your car's steering wheel an implementation detail.
Yes, if there weren't so many important decisions behind the outcome of a car being steered with a steering wheel, it could be a steering handle, or a steering joystick, or just about anything else that allows you to orient the front wheels of the car. The same is true of the pedals on the floor. Those could be implemented as controls on the steering wheel instead. Whether it's an implementation detail depends on the specificity of the feature in question. When I asked you about an "implementation detail", it was scoped to "the feature is pattern matching" (can I steer the car?) and you scoped it to "the feature is pattern matching without overwriting variables conditionally in surprising ways" (can I steer the car with failure modes that aren't fatal?).
and then all that?
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`).`not_found` behaving differently from the literal `404` breaks an important principle: “if you see an undocumented constant, you can always name it without changing the code’s meaning” [0].
[0] https://twitter.com/brandon_rhodes/status/136022610839909990...
Worst of all, though, it’s really just another way to write if-elif-else chains that call `isinstance` - a pattern which Python traditionally discouraged in favour of duck-typing.
1. https://pythonsimplified.com/the-most-controversial-python-w...
But for the sake of maximum pendatry let me paste some nitpicky little detail from a somewhat recent syntactic addition:
>>> def f(a, b, /, **kwargs):
... print(a, b, kwargs)
...
>>> f(10, 20, a=1, b=2, c=3)
10 20 {'a': 1, 'b': 2, 'c': 3}
a and b are used in two ways.
Since the parameters to the left of / are not exposed as possible keywords, the parameters names remain available for use in **kwargs
Jesus fucking hell on a tricylce so now i have *'s and /'s showing up in function signatures so someone can prematurely optimize the re-use of variable names without breaking backwards comparability?!Python is becoming a mockery, dying a death through a thousand little cuts to its ergonomics.
Avoiding tedious boilerplate by adding nice features like the walrus operator is precisely what lets us avoid "death through a thousand little cuts to its ergonomics", in my view.
Sure, maybe writing
m = re.match("^foo", s)
if m != None:
...
isn't so bad, but in that case maybe writing i = 0
while i < len(stuff):
element = stuff[i]
...
i += 1
wouldn't be so bad, and we could get rid of Python's iterator protocol?The iterator protocol is way more general than what you have; it's not remotely comparable.
if foo := data.get("foo"):
handle_foo(foo) foo = data.get("foo")
if foo:
handle_foo(foo)
The only place I have found the walrus operator useful is in similar `while` loops, where the equivalent code would be: while True:
foo = data.get("foo")
if not foo: break
handle_foo(foo) for foo in iter(partial(data.get, "foo"), None):
handle_foo(foo)
So I would only use the walrus operator for the first example (the if statement), which even though it is exactly the same as doing it in two steps just feels nicer as a single step.Python has always been strongly typed (Python has strong dynamic typing). Adding typechecks moves it towards being gradually/statically-typed.
On that list: the walrus operator and the new switch thing. If I understand them fully and correctly, those two things don't enable developers to do things that were impossible before, instead they add new ways to do things that were possible prior.
That's the Python I know and love.
Of course, this doesn't mean I'll love Python any less, just that I wished there were more focus on staff that matters like the topic of this article. Or maybe getting type hinting better.
Again, this is just my opinion.
After using pattern matching in Rust and switch statements in JavaScript, I personally am very excited for that addition to Python, but I understand the feature is divisive and will concede it as a matter of opinion.
Edit: turns out the walrus operator does not cause the variable to move out of scope after the if block, which is disappointing. IMO the worse anti-pattern has already been part of the language, which is not creating new scopes for if statements.
Python always had at least half a dozen ways of doing trivial things such as packaging dependencies or even doing http requests or looping over stuff.
But yes, of course can one also argue that they add friction on the global scale, because it's yet another syntax-element to know about, and the benefit is rather small on surface. But that's the problem with syntax, it's always a trade-off between overhead and benefit.