168 comments

[ 2.9 ms ] story [ 196 ms ] thread
Ah, brings to my mind the classic talk: https://www.destroyallsoftware.com/talks/wat
That one was all cynicism that baited the experienced and confused and discouraged beginners... this one actually takes the important next step of making it all teachable which is really great.
Oh come on Gary literally runs a top notch teaching site and the link was a bit of fun on his previous teaching site.
Well he also started a spread of snark that made the entire industry look bad. His own site is called destroy all software. Even if that is meant to be ironic, even that irony isn't helpful.
I sincerely think you’re taking Gary and the software industry too seriously. Wat is nowhere near as reputation damaging as, say, “PHP a fractal of bad design”, and PHP is doing great. People have aped the talk to highlight rough corners in their own preferred ecosystems. It’s fun. Programming is dealing with a lot of broken stuff. Embrace it and have a laugh.
I did that day I first saw it, and then after three years of people pointing and laughing at how awful computers are for upvotes and likes it stopped being funny.
They’re not laughing at you, in case you need to hear that. And it’s not computers that they’re laughing at either. It’s human mistakes, misjudgments, and so on. I’m exceptionally talented as a software developer and I make absolutely boneheaded decision, designs and mistakes sometimes. I find it comforting when I just accept my imperfections. I’m more likely to attach an everythingisterrible.gif to my own PR than anything else. It’s made me feel more at ease with myself and my craft to lighten up and have a casual cynicism about the whole thing.

Computers aren’t bad but most of what happens on them is. And that’s okay. Most of everything is bad, in varying degrees. At least the silly bad stuff brings a smile to people’s faces.

No I totally get this. But the cynicism has been relentless since this online, and contagious, and many use it as an in joke, and this is serious stuff. People's careers... 6 hour conference calls involving tens of experts trying to get to the bottom of these things.

Most of these things are just people doing things outside of the language spec and then are somehow surprised when it doesn't work and then make a joke of it all and I don't see how that helps anything.

Again, it was funny at the beginning, but now I really wonder if these people just want to hurt other people by being abusive with this incessant snark. Perhaps this an Eternal September issue, so maybe you're somewhat new, but shitting on things is easy.

Oh no, someone was being funny! Think of the beginners, they can't deal with that!

The wat talk is funny and educational. I watched it with just a couple of weeks of experience in JavaScript, and I learned a lot from it. There are other great resources (the good parts, the you don't know js series, etc), but none of them offer value so quickly.

The wat talk is a great reminder to learn more about the language you are using and start reading the aforementioned books.

Oh no, more sarcasm. If only I was in on the joke /s
I already had the song NaNNaNNaNNaN watman in my head before I clicked the link.

Edit: okay this thing that gave me a little smile deserved a downvote for some reason I guess. Hope whoever else comes along and doesn’t like a cute fun thing or me enjoying it can explain why.

You do know what they say about Watman - he's not the hero we deserved but the hero we need. Have a nice day!
This is a really impressive list of oddities, with solid explanations on every single one. Excellent resource, thanks!
all these cool new features! what's the thing they always say about python? "there's more than one way to do it"?
Our old friend, Tim Toady. He has never left us, even though Python did try to pour bicarbonate on him.
That's a Perl slogan but I suspect you already knew that
Python was always a second-rate imitation of Perl. Camel > Python.
yep, corresponding python slogan from PEP-20:

"There should be one-- and preferably only one --obvious way to do it."

this was the compelling argument for python to those who were struggling with large and hard to maintain legacy perl codebases. python was supposed to be a constrained, simple language that encouraged readability- ideally a solution to the problems everyone was facing.

history really does appear to repeat it seems, even in computerdom where the timescales are shorter.

Ah yes, the one about mutable default arguments was a real surprise for me once in a debugging session. Python only initialising them once during the whole script runtime is not exactly the behaviour anyone would expect.
They're also an escape hatch,

    def main():
        funcs = []
        for x in range(10)
            def f(x=x):
                return x
            funcs.append(f)
        print([f(x) for f in funcs])
The "x=x" default argument is necessary here. In this example it's just a constant but this trick is often used with mutable defaults.
You're talking about something different. The parent comment is about mutable default arguments are shared between all calls to that function [1], but you seem to be talking about functions defined in for loops all sharing a reference to the same variable [2].

[1] https://github.com/satwikkansal/wtfpython#-beware-of-default...

[2] https://github.com/satwikkansal/wtfpython#-loop-variables-le...

> In this example it's just a constant but this trick is often used with mutable defaults.

Your example doesn't work with mutable defaults. Ironically it falls into the trap the parent comment was referring to!

    def main():
        funcs = []
        arg = []
        for x in range(10)
            arg.append(x)
            def f(arg=arg):
                return arg
            funcs.append(f)
        print(*[f() for f in funcs], sep="\n")
        # result: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] printed 10 times
These functions will all return the same 10-element list. Instead you need to use `def f(arg=arg[:])` or `def f(arg=copy(arg))` (after from copy import copy).

> print([f(x) for f in funcs])

Just a typo but you meant print([f() for f in funcs])

These are two manifestations of the same feature: function arguments's default values are evaluated when the function is defined.

In one case, the value is a non-mutable int, while in the other it's a mutable list, but in both cases each call to those functions will share the same value for that argument (unless overriden).

> You're talking about something different.

No I’m not. Exercise for the reader to figure out why. The key thing here is that you want a mutable variable for each closure, but not shared between loop iterations.

[Edit: deleted my original reply here. I had ended in "Are you sure you didn't misread the original comment?"]

Ah ha! I misread your comment! This was the bit that confused me:

> They're also an escape hatch,

I read this as

> There's also an escape hatch,

So I thought you were saying that the code in your comment was a solution to the problem posted by the original comment. This is why I found your comment so bewildering! I now see that what you were saying that this gotcha (Python evaluates function default arguments, once, at the point of definition) also has a use.

So, I have to say sorry about that. But I also can't help saying your comment could've been phrased a bit less confusingly.

I feel like WTFs caused by doing something deeply weird hardly counts against the language.

Yes doing the walrus operator inside brackets works. Yes of course it's going to be doing weird things. Is this really surprising to anyone? It's not what that operator is for.

Yes comparing strings with "is" works sometimes. It's not checking equality and it isn't supposed to. In another implementation using "is" might work all the time or none of the time. Don't use "is" to compare strings. it's not what that's for.

Yes chaining mismatching operations does weird things. Be clearer in how you write your code. It's no surprise that writing confusing code results in confusing results.

I've been seeing a growing anti-python sentiment lately, and most of it seems to be based on these sorts of odd perceived language defects. Python is by no means perfect, but none of the code on this page is something anyone would actually write in production code on purpose.

I suppose it's a sign of python getting more popular.

> I've been seeing a growing anti-python sentiment lately

Despite the name, I don't think this is anti-python, in the same way classics like [0] are anti-PHP. The project README mentions this a bit:

> While some of the examples you see below may not be WTFs in the truest sense, but they'll reveal some of the interesting parts of Python that you might be unaware of. I find it a nice way to learn the internals of a programming language, and I believe that you'll find it interesting too!

0: https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/

Fair enough I suppose, but I'm still seeing posts like the OP getting linked all over the place by people trying to disparage python as a language.

Of course once you get more deeply into a language it's probably a really good thing to get to know all the weird edge cases. One of my favorite past-times is writing intentionally terrible code just to exercise some of those edge cases. For instance this hello world code uses quite some oddball language features: https://gist.github.com/SuperDoxin/fde4fdad237e85a90c8764cdf...

this is the same defense against the php and javascript posts: if you know the entire language really well none of these are actually problems
If you'll read more carefully you'll find that that's not what I said. I said none of these are problems you'll run into. Even as a newcomer.

Once you're getting better at the language it's nice to know these edge cases because they teach you details about the language you'd otherwise miss out on.

I agree, it’s the same thing when “WTF JS??” articles always include behaviour that’s part of the floating point spec in their list.

I can’t think of any popular language that doesn’t have weird quirks.

That said, the content of the post is useful. Knowing about this behaviour, even when it’s demonstrated by using language features incorrectly, is important. I’ve seen enough code that misuses “is” on strings to know there’s a need for information like this to be collated and accessible.

This information is definitely useful, though it probably shouldn't be presented in such a newb-friendly way. It might very well dissuade people from using python based on nonsensical reasons.
I agree with most of your points, because in most cases, people try something very weird, and they are surprised that the result is something even weirder (or they just didn't think through the example logically).

Except the string comparison with "is". It's confusing, because there are no errors, and when I try them in the Python REPL, it "works". The code is logical, I tested the example and it works...

...except when it doesn't, but if I'm just a naive beginner, I'll need to learn the hard way that it doesn't always work

For this reason, I'd be okay with saying that "it" for strings is a WTF of the language.

The confusion there is caused by english not disambiguating between equality and being-the-exact-same-thing-ness. I'll grant that "is" is a bit of a footgun for new users, but it's not a WTF in the sense that the language is doing something weird.

Rule of thumb is use == for everything except True/False/None. Once you get going with python you'll learn the meaning of is eventually, and you'll still barely ever need to use it.

Why that exception?
None, True, False are singletons and are never constructed again after initilization of the interpreter (and have been keywords in python2). That's why "is" is always safe.

Small integers, as they are kept around, probably are as well (5 is (2+3) works).

I think what gets dropped during the conversation is, that we've been living with Java doing it this way (is being == and == being .equals) for decades and nobody really complains.

(comment deleted)
I mean why not use == for everything?
1. it's a slower check, just like equals() intended for value equality is slower than reference equality == in java.

2. 1 == True and 0 == False, as booleans subclass the int type. On the other hand is distingushes between 1 and True, and 0 and False respectively.

Noob question: instead of using == except for True/False/None, what about just using == for all comparisons?
In python, strings are mostly unique, meaning that a string can be bound on many variables, and all the variables point to the same string. This is where the 'is' operator comes in, 'is' checks for reference equality, that is, the object that is bound onto the two variables is the same. If they are not the same, 'is' fails.

In other words:

    x = "foo"
    y = "foo"
    x == y and x is y
Both strings point to the same exact memory. Hence, 'is' works. But it isn't always true:

    x = "foo"
    y = f"{x} "[:-1] # get everything but the space
    x == y # True
    y is x # False
Checking for reference equality (comparing pointers) is such a niche requirement it should be hidden behind stronger syntactic vinegar. The greatest mistake Java made maybe is having == for testing pointers, and .equals() function for actually comparing.

Every JVM language that came after Java reverses this. Here, "is" simply is too nice. Should've named it something nobody will accidentally use.

(comment deleted)
> Checking for reference equality (comparing pointers) is such a niche requirement it should be hidden behind stronger syntactic vinegar.

In python it’s common due to `None`, which you really want to check by identity. Other placeholders as well. This is such a major use case that in 3.9 `is` was moved out of the generic `compare_op` instruction and into its own.

And because __eq__ can be overridden, aside from being noticeably slower `x == None` is not truly reliable.

It’s not even that. CPython happens to sue memory address as ID, but the language doesn’t require that. You could easily use something else for the object ID and get completely different behavior.
Serious question: how do you know how the language is intended to be used?(or what the one right way is). Last time I checked, I couldn't find a document about this.
Most likely by interacting with other python developers once you run into trouble doing things in odd ways. Same as for any other language.

Main difference is that the python community has a rather strong reaction to how pythonic your code is. You'll notice soon enough after working with python that for most python developers "it works" isn't good enough reason for anything. It has to work and fit how you're supposed to write python.

Some of it is encoded in style guides like pep8, but most of it is pretty hard to codify like that.

> I've been seeing a growing anti-python sentiment lately

The same happened to Ruby and Rails. Once the last "in" thing no longer gets attention and new "in" things are the talk of the blogosphere, "yesterday's" things start to get dissed.

I think it's to gain points for recognizing the new thing. To attract people following "old" thing to the "new" thing and provide social validation or increased mindshare for one's choices.

Or maybe people are tired of pain points with the old way (even if just perceived or tangentially related), and they need to lash out at something.

To be fair to Ruby I think it has a lot less WTFs than Python. The design of Ruby is a lot cleaner.
Can you imagine same "yes - ..." comments made about some new, straight out of the oven language? Most people would not touch it with a ten foot pole.
And it'd be unfair to that new language too. Basically all of the WTFs in the OP are things you don't run into, unless you're writing deeply deeply weird code, at which point it's hardly the languages fault that it does weird things.
Not a python dev, but what is deeply weird about the examples in "Strings can be tricky..." section?
Both "id()" and its companion "is" are almost never used in practice - I don't think the tutorial mentions them. At least when it comes to id(), you'd have to find that in the reference where it will explain how it works.

If you're familiar with C, id() returns the memory address of the thing given to it, and "x is y" is like "id(x) == id(y)". So "x" is "x" is asking whether the Python runtime happened to put the two strings in the same place in memory, not whether the two strings are equal.

("is" is commonly used for one thing - testing against None. So you might write "a is None". I think that's just because someone once got bitten by a badly overloaded == operator. Just like some people will write === in all their Javascript, in case an incorrect type sneaks in.)

(comment deleted)
> I've been seeing a growing anti-python sentiment lately

Not speaking for the quality of the article, but on that sentiment: maybe it's just that the pro-python sentiment is eroding. I'm glad it's becoming kind of ok to criticise that language, after years of it being untouchable (at its core, not the 2 -> 3 migration). At least that's my sentiment as someone who really doesn't like it (but I'm a professional, I still use it when it's a best tool for the job, it's just a pain). I'm sure just saying that is still suspicious, and saying that I prefer JS (modern) to Python will still get me more than raised eyebrows for at least a few more years.

> JS (modern)

LOL. JS is what, 25 years old, versus Python at 30.

I think the reference is to modern JS as in ES6 or TypeScript
LOL, it's about "modern JS", not JS being new.
> I'm glad it's becoming kind of ok to criticise that language, after years of it being untouchable

I've only been following Python for a little over 20 years of it's ~30 year life, but I don't remember it ever being “untouchable”, or even just “untouched” by criticism.

Up until mid-2000s, no one had heard of Python. Then there was maybe ten years of “Whitespace? No!” But from say 2012 on there was a big movement of data science and education into Python and its general reputation was quite good.

Edit: the XKCD antigravity comic was December 2007, so pretty early on all things considered. PG’s Python Paradox was 2004.

> But from say 2012 on there was a big movement of data science and education into Python and its general reputation was quite good.

When Python became popular in education, particularly for first CS courses, is when the public criticism from Scheme, Java, and and C partisans really skyrocketed.

Yeah, it has had recently a generally good reputation. That doesn't mean that it has been untouchable and immune to criticism.

I’ll agree with that. I think it’s more that the criticisms of the loudest voices has shifted. Once it was “why use an obscure language” then “ugh whitespace” then “dynamic typing is bad” and now more “it’s jumped the shark.”
>Up until mid-2000s, no one had heard of Python

You'd be surprised. Python was used quite a lot in early 00s.

Ruby is the one that was a curiosity few used at that point, until Rails came.

I've observed the weird phenomenon that any valid criticism of Python is treated as either some kind of personal attack or because you're a mad static typing ideologue.
Adding to the list “oh if it’s not fast enough, just write it in C”

As if wishing for any performance that’s an improvement on bottom-of-the-barrel is some kind of wild, unreasonable ask for which the solution is “write it in a language full of pitfalls, sharp edges and an even worse packaging and developer experience than the current one? Seriously?

To be honest I feel like most criticisms against any languages are bound to sooner or later be met with the response "well, this is how this language works, and if you don't like it pick another". If you criticize a weakly typed language for an issue that is solved by strong typing, you could even argue that they are correct in saying so, unless you come up with a way to solve it without types.

I suspect that the reason that it's so common to be initially suspected of being an ideologue is that there are so many out there, and they are very vocal compared to the majority that often couldn't care less. So if you don't come up with alternate solutions, maybe the tendency is to assume you're getting at the old trope they've already heard before.

As an example I recently had someone say that "best practices" are just as good at avoiding the kinds of things a static type system catches. Python seems to be (for whatever reason) a language that creates this kind of ideology with the kind of tropes everyone has heard before.
You lost me at: “I prefer JS (modern) to Python”. Just... how?
What "how"? Have you modern tried server side JS?
Python was my preferred language for many years, and I still use it for processing CSV files with low development time, but JavaScript has gotten so much better now that I prefer it overall. Python has a better ecosystem but JavaScript has a better dependency management story, so those two factors sort of wash each other out.

  >> none of the code on this page is something anyone would actually write in production code on purpose.
I mostly agree, but there is one example where I thought it is important to know and remember:

for x in range(7): def some_func(): return x

Here x binds to the (outside) symbol x, not the value of x at creation time of the inner function.

Yes - in many languages x is a new variable on each iteration of the loop. Python re-uses the same variable each time, because the loop does not have its own scope.
I’ve seen a number of experienced python programmers bitten by this. They expect a new scope but don’t get one.
That's how I expect it to behave. But anyway, the alternative (x in a new lexical environment each time the loop body is entered) only makes sense in a language where variables are scoped to a lexical block, not in Python where variables are scoped to the whole function.
Critiquing X, or even warning people about surprises in X, is not "anti-X". Have people picked up this weird idea from fandom culture or something? This article is not saying that people should not use python. It's not criticizing the maintainers for making stupid decisions, as far as I can see. It's just saying "if you do this, you'll get a surprising result", and in the process teaching people more about how it actually works.

One of the most informative books I read early in my career was "C traps and pitfalls". I've written a lot of C in the subsequent 20 years or so. It has some particularly nasty traps. Acknowledging that does not make me anti-C.

Pointing out that a saw is sharp does not make you anti-saw.

> I've written a lot of C in the subsequent 20 years or so. It has some particularly nasty traps. Acknowledging that does not make me anti-C.

depends on your cultural background I guess. Some people see literally everything that is not 100% risk-free as inherently bad.

Well, possibly, but if you genuinely think that then you can never achieve anything other than complaining on message boards and hodling US treasury bonds. Everything has risks and it's a question of enumerating them and deciding how and whether to manage them.
> if you genuinely think that then you can never achieve anything other than complaining on message boards and hodling US treasury bonds.

I mean... yes. That's, like, most people.

It doesn't have to be 100% for that to be right.

If we can achieve a N% risk-free Python (without sacrificing other useful features), and we only have a M% risk-free Python (where M < N, or even M << N), then what we have is inherently bad.

> Yes doing the walrus operator inside brackets works. Yes of course it's going to be doing weird things. Is this really surprising to anyone?

Yes, it is in fact extremely surprising that an expression does something different based on surrounding context. It would be as if expr had one meaning but it you called f(expr) it had another. Saying that you're using it unexpectedly doesn't really address the issue of language design.

It reminds me of Matlab's idea (a long time ago) that f(i) is either a function call or an array access at index i, but you will not know for sure until you've evaluated f, so expressions like f(i)(j) end up being just weird. There is a certain amount of logic that you are fully justified in expecting from a language.

> It's not what that operator is for.

If your idea is that an operator is free to do whatever, once it's outside its predicted situation, then I can see how that's technically correct based on the language spec, but it just seems like a very weird way to just a language design.

I think if you think that some of those examples have no legitimate uses at all you'd then have no problem saying that they should be syntax errors but are accepted anyway, that it's just a bug in the parser, that it accepts too much.

I also do not see how this is anti-python, I think you are being unfair to the author.

> It is in fact extremely surprising that an expression does something different based on surrounding context

It isn't doing anything different though. Compare:

    >>> a:=1
    1

    >>> (1, 2)
    (1, 2)

    >>> (a:=1, 2)
    (1, 2)
The thing that's confusing people here isn't even the walrus operator, it's tuple syntax.

> If your idea is that an operator is free to do whatever, once it's outside its predicted situation

No it's not. But clearly an operator can never do an operation it isn't defined to do. is and == are different operators and they do different things.

> I think if you think that some of those examples have no legitimate uses at all

They do have legitimate uses, but purposefully obfuscating the context around what the code is doing is hardly the fault of the language.

> I also do not see how this is anti-python, I think you are being unfair to the author.

Yeah on further thought it really isn't anti-python, but articles like the OP do tend to get linked in anti-language posts, which is probably what got me a bit annoyed about the article. No fault of the author.

> I've been seeing a growing anti-python sentiment lately, and most of it seems to be based on these sorts of odd perceived language defects.

I've seen the same, but to me it feels like the downslope of the hype-train that was brought on by the ML crowd. Early on there was python doing python things, and the (IMO overly smug) community was happy with that. Then the AI/ML hype train fired up and for historical #reasons python became the language of choice, and this introduced a LOT more people to the language, and it became their lingua franca.

Now, those people are having to write honest to god applications and are finding that python is Hard To Scale, both technically and code-management-y.

IMO. Reasonable people disagree.

"Hard to scale" isn't really a coherent argument against any mainstream programming language.

It's kind of like saying that AMD processors or Microsoft Word are hard to scale - it's not entirely meaningless as a criticism but it's close to it.

Python is not a language for writing ultraperformant multi threaded code but, of course, that's not what it used for or was ever intended to do and is an entirely separate issue to scalability.

Reasonable people disagree about well defined, important matters.

> "Hard to scale" isn't really a coherent argument against any mainstream programming language.

It is odd that you would say that, because it is a very common complaint about languages with dynamic typing. As the program grows larger static typechecking becomes more and more helpful.

That is one of the many interpretations of "not scalable" I suppose...

Regardless, python has tools to do that. Instagram and Dropbox in particular have written quite a lot about their internal usage and built and open sourced additional tooling (e.g. MonkeyType - that lets your code type annotate itself).

Fact that they had to do that does prove the point, doesn't it?

How about organizations that hit the scaling point, but are not big enough to invest into tooling like that?

No. MonkeyType is open source.

It's exactly the kind of tooling that makes your life easier if you have multiple teams and wanted to start enforcing static type checks with MyPy and having an IDE that understood your types.

So before it was open sourced, all those companies that couldn't pull it off themselves had scaling problems or not?

So before Python had types was it hard to scale or not?

Seems like you are defending "Python with types and lots of tooling" rather than Python "nice and simple language (until not)".

My theory is you didn't read past the couple of pages. There is some very interesting material in there that was news to me and I've been doing Python fairly intensively for the last fifteen years.

Edge cases are really interesting. It's important to know them, and it's no comment on the language.

I don't understand these comments suggesting this article is anti-python. I won't say it is the opposite. But the repo is literally stating the goal is to "Exploring and understanding Python through surprising snippets.". And I totally agree that analysing and trying to understand what is actually going on, when the language is behaving in a slightly unexpected way, is a great way to learn. So see this as a resource for people who want to learn, not for people with an “anti-python sentiment” (whatever that even means).
I think most of this is covered by the python rule of thumb "explicit is better than implicit". I have a particularly stubborn cohort who loves nothing better than doing what I call "cargo trains" of combining list comprehensions and "." into sometime 7 or 8+ connected items in one liners. It is ridiculous. I have actually set calendar reminders to go ask him what these trains do after a couple of months between when he wrote the code and $TODAY. It never fails that he'll "get back with me". About an hour later I'll get an explanation via email :) . It never teaches him anything though.

    >>> another_tuple
    ([1, 2], [3, 4], [5, 6, 1000])
    >>> another_tuple[2] += [99, 999]
    TypeError: 'tuple' object does not support item assignment
    >>> another_tuple
    ([1, 2], [3, 4], [5, 6, 1000, 99, 999])
This one's my favorite. Although it's a rare issue that you're unlikely to come across, it's a side-effect of a much bigger design flaw in Python - the decision to make `a += b` behave differently from `a = a + b`.
It's a design decision to allow it to behave differently, as you can implement += with a more efficient in-place version, instead of a copying one where the lvalue might be different/new and you have to keep the old a.

I even think this is good design, as by convention + does not mutate its arguments but += can be a fast mutating version.

I guess reference counting makes copying a list in Python a much more expensive operation than it would be in other languages. Although perhaps it could also be used to avoid copying in cases where the list's own reference count is exactly 1?

Anyway, there's nothing wrong with offering an operator to change a list in-place, but IMO `+=` is the wrong way to spell it.

> but IMO `+=` is the wrong way to spell it.

Again, operator overloading. __iadd__ just calls append. Many a library might do something much better. But as it looks like the in-place version of +, even for lists it seems fine (although I've been missing a concat-operator in python for years - seems we're stuck with +).

I'd say it's even deeper - the += operator is fundamentally tied with Python's semantics. Consider:

    >>> a = [1, 2]
    >>> b = a
    >>> a = a + [3]
    >>> b
    [1, 2]
It has to be [1, 2] because the third line creates a new object. If a + operation changes its operand, then the concept of object identity breaks down - at least, it will be a very different language.
You’re right - I’m not suggesting that `+` should work in-place, but that `a += b` should do the same thing as `a = a + b` and not work in-place.
So you're saying that a += b should create a new object and assign it to a? Now that would be even more unexpected...
I don’t think it’s unexpected for an augmented assignment operator to do an assignment. It is unexpected for it to instead call `extend`.

It’s even worse to do the first for some types and the second for other types.

That's a wart and a half. It puzzles me to no end that this would be an error - `another_tuple[0].append(3)` is not an error.
It’s because `another_tuple[2] += [99, 999]` is equivalent to something similar to `another_tuple[2] = another_tuple[2].__iadd__([99, 999])`. The list is modified in __iadd__ but then the assignment fails.

The assignment is necessary in case __iadd__ doesn’t want to modify in-place and instead return a new value. It’s this “sometimes modify in-place, sometimes don’t” which causes the issue.

While this repo is great for education, it doesn't cover the deep problems with Python that you only discover when you try to do something "unapproved" or slightly off the happy path. Depending on the domain you are working in you may never encounter such a situation, but if you do, be prepared to lose your sanity one omnipresent design flaw at a time.

Over the years I've been collecting a series of cases that are somewhat more nihilistic and depressing under the general category of LOL PYTHON. The little inconsistencies, the boilerplate that is required if you don't want to use only the approved, not performant, and breaks all sorts of other hidden assumptions so you can't actually use the code outside very narrow settings approach, eventually just evoke a nihilistic chuckle and a note to never start a new Python project again.

I think you forgot the link to your collection?
There are some that you can find in [0], but they are often de-contextualized (and sometimes aren't really Python's problem, because there was a trade off that had to be made, and the LOL is just frustration as a result of the bad ergonomics that it creates), those and some others will eventually go in an longer post when I finally get that set up.

To give one concrete example of an LOL PYTHON consider the @property decorator. While it seems like a really cool and handy idea that could let you save some typing in some cases, it is actually a trap that you should almost never use because when you inevitably discover that there is some parametrization that you need to pass to the ... attribute, you are suddenly faced with a massive refactoring. Also if you ever make the mistake of having boolean properties such as isDir or isUrl and somewhere else you have a predicate that is a function instead of a property then isFile will always return True when you meant isFile(). The only safe solution is to never use properties/attributes as predicates. These are nasty footguns that seem obscure and unlikely to affect you until you have the misfortune to already be in the briar patch.

0. https://github.com/search?q=%22LOL+PYTHON%22+user%3Atgbugs&t...

If I understand correctly, I think the problem with your second example is that you should avoid checking for `True` with something like `if isFile` but instead `if isFile is True`. That would solve this specific problem (and is also slightly more efficient because no implicit boolean conversion is performed).
You can do that, but loose checking for truthiness is pretty pythonic. It's weird to put "true" in there if you think you're checking a boolean property. If you know you're not, then you realize you need to call the function and then you can remove the "is".
This one is a bit more subtle. Assume there are two classes on that uses @property and one that does not.

    class Here:
        @property
        def booleanPredicate(self):
            return random_bool()


    class There:
        def anotherBooleanPredicate(self):
            return random_bool()
    
    here = Here()
    there = There()
What happens in the following cases?

    here.booleanPredicate           # as expected
    here.booleanPredicate()         # loud failure
    here.anotherBooleanPredicate    # silently always True
    here.anotherBooleanPredicate()  # as expected
So you might think that you should _always_ use @property for boolean predicates, except that there are libraries that use the anotherBooleanPredicate version so you are now at risk for silent always True behavior (creeping sanity destroying evil behavior). To prevent the silent failure you should always start by calling the predicate so that it can fail loudly. Worse if someone does a refactor and misses going from @property form to function form then the silent failure literally cannot be detected by running the program (!).
I am not trying to be edgy here when I say that I have viewed a lot of the "syntactic sugar" business with a great deal of skepticism. Piggybacking off of your example, upon reflection I believe that me looking askance at decorators is partially a learned fear (I escaped Perl), and partially because decorators seem a little like "bad magic," where you have cast a handy but seemingly innocuous spell only to find that it has deeper consequences later.
I would be so keen to read this if you ever document them somewhere!
That's the flip side of "There should be one— and preferably only one —obvious way to do it."

Engineering is the art of trade-offs. Same problem with different requirements needs a different solution.

There is, its called C /s
True, but the problem is that in pursuit of that mantra the language has wound up disempowering engineers and has forced even more decisions about trade-offs onto them: do you want readability, performance, or maintainability? You usually can't have all of them because of some lurking performance tar pit.

An example here is the conflict between needing top level imports for sanity and static analysis (there was an instagram blog on this a while ago) while also wanting fast startup time. If you import requests at the top level then you cannot have fast startup time, so you wind up importing it outside the top level so that code paths that don't use requests don't have to pay the time cost of loading it, but then you have imports that aren't at the top level!

edit: oh, and don't even get me started on pkg_resources [0], which isn't just a tarpit, it is a full blown blackhole beartrap waiting for the unsuspecting.

0. https://github.com/pypa/setuptools/issues/510

Python should just remove the `is` operator. If you really need to compare object identities, which is already a rare scenario to begin with, you can explicitly compare the object IDs. Checking if something is or is not True/False/None can just be treating the thing as True-ish or False-ish. Making the language bigger is not nearly as impressive as making it smaller!
I find checking for "is None" very useful, but that is also the only time I use 'is'. I try to avoid checking for True-ish and False-ish whenever possible, preferring to be more explicit.
I agree about explicit None checks. It is often useful to distinguish None from the zero value of a type such as a numeric zero, empty string or empty collection. I also write "is None" & "is not None" everywhere when doing these explicit checks.

Yet the same be benefits of explicit None checks can be attained by writing "== None" i.e. doing value based comparisons against a None value. So this argument does not explain why "is" is necessary.

I think the reason not to write `== None` is that classes can override `==`:

    class C:
        def __eq__(self, other): return True
    C() == None  # True
    C() is None  # False
It is also marginally faster, I think.
Just did a quick check, and looping over an array of 1 million Nones and checking each element

  el==None
was roughly 25% slower than

  el is None
edit: Doing

  None==el
was roughly 15% slower
> Yet the same be benefits of explicit None checks can be attained by writing "== None"

If you are going to do that's you should probably do a backwards comparison (`None==x`), otherwise you are calling x.__eq__(None), which probably won't be surprising in result if you have constrained things with sane typing, but still could be more expensive than `x is None`.

`x is None` reads a lot better though.

(comment deleted)
Yeah, IME `is None` is by far the most common use of `is`. I don’t think many people would miss `is` if it were removed and an `is_none` function introduced.
> Checking if something is or is not True/False/None can just be treating the thing as True-ish or False-ish.

No, it can't. Especially obviously not something whose type is Optional[bool]. Or, more subtly, Optional[Number] or Optional[Sequence[T]].

(comment deleted)
Oh look. Another techie trying to gain cred by hating on things.
The Python/NumPy gotcha that has caused me the most pain by far is

  def foo(x, y):
       return x+y
where the intention is for x and y to be NumPy arrays, but the caller accidentally passes lists (or vice versa). This is especially common because a lot of NumPy functions are agnostic as to whether they operate on arrays or other iterables.
I've been bitten by this as well. In general, having two types that are mostly, but not always, interchangeable, often leads to hard to debug problems. A similar situation that comes to mind was str and unicode types in Python 2.

Sometimes you end up with a stray value of a wrong type somewhere in a complicated data structure. It gets propagated down the line since most parts don't care if it's one type or the other. This makes it possible for the value to propagate far away from the original mistake that caused it to be of the wrong type in the first place. Finally when some code chokes up on it, it can be completely unrelated to the code that needs fixing.

Since it's hard to debug the original cause this is often fixed by just adding the type conversion at the place that choked up. But this is just kicking the can down the street, since the remaining type inconsistency will soon pop up a problem somewhere else.

That's clear example of bad language design. Overloading operator syntax and breaking semantics in dynamic language is big no-no. It may not be good for statically typed languages either, but at least they can get away with it.

Convention of using + for joining sequences and addition is human language level abstraction. It breaks in programming.

In formal theory strings over an alphabet form a semiring, but (*) is used for concatenation and (+) for alternation. Not applicable anyway.

Last, no clear syntactical cue to distinguish operators acting on elements from those acting on objects.

I disagree.

The intention in foo is not for x and y to be numpy arrays, it is to add 2 objects together. "If it walks like a duck and it quacks like a duck, then it must be a duck".

That function can work on anything implementing +, this is the philosophy of Python. It enables the whole numpy, dask, TensorFlow ecosystem.

If you intend to limit it to numpy arrays, you either use numpy 1.20's static types, or you add more checks at the beginning of the function, such as np.asarray(x).

To be frank I think the python community lacks self-awareness.

There's this ritual you have to do when learning python: blindly accept status-quo design choices as "universally good" (but call it "pythonic"), and crap on other languages. Never admit python or the ecosystem has issues or shortcomings, and if another language has something better than yours (eg, Yarn, or Composer is way better than anything in Python (poetry is a good step but not a competitor)), or offer really good IoC tools: just double-down on the status-quo and say something like "looks over engineered, I see no reason why I would need that".

Pythonic is putting a decorator everywhere, coupling the system together into a ball of mud where you can't decouple it. Pythonic is having annoying mixed naming conventions. Why do I need methods to have underscores in 2021, but classes to use camel case? Why do my lines of code need to fit on a punched card invented decades before the language was invented? Why does the idiomatic "one true way" need to be what some other engineer thinks is "the one true" way?

Python is a fractal of closed-mindedness and single-use code. There's a reason it took a decade to get projects onto python 3, but PHP can swiftly move the entire ecosystem from v5 to v7 in a year. FastAPI is getting a lot of praise recently, but frameworks of this style are a dime-a-dozen in other languages and have been around for a decade.

Pythonic is a meaningless word that just gives firepower to the most confident-sounding engineer in a company to shoot down anyone who doesn't code like them. It's not an objective concept, it's innovation poison.

> However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. When in doubt, use your best judgment. Look at other examples and decide what looks best.
My issue is with the behaviour of humans who belong to this hegemonic cult, not the document.
> Why do I need methods to have underscores in 2021, but classes to use camel case? Why do my lines of code need to fit on a punched card invented decades before the language was invented? Why does the idiomatic "one true way" need to be what some other engineer thinks is "the one true" way?

These problems are applicable to all languages, not just Python.

To rephrase: Different engineers and different problems have different needs. When you ignore this, you'll get something that someone new can pick up, read the docs, and work with... but you can end up with a slight mismatch between tool, task, and user.

In some contexts, that slight mismatch can escalate to become quite wide.

A deeper examination of Django would yield rich examples of this.

------------

The above is really a message for myself as an engineer who loves python and has an emotionally visceral distaste for PHP. I offer it to y'all in case you also find it useful insight.

Python: where relying on “lol magic” is acceptable thing to have in production code. At least “magic” in languages like Haskell has the dignity to be backed up by a type system with more consistency than a wet noodle.

Python can’t seem to decide whether it wants to be a production grade language or easy-for-beginners and keeps implementing pointless features (looking at you walrus operator and weird version or pattern matching) instead of fixing its actual problems like the tire-fire that is packaging and performance.

“But Python has never been about performance” someone will say, and that’s fine, but it’s increasingly becoming a language that isn’t any more ergonomic to use than its peers, with worse packaging and deployment, useful tools (type hunting) that the community seemingly refuses to adopt widespread and its slower on top of all that?

Why would you actually bother?

> But Python has never been about performance

I have been burnt by this. As much as I like python's "expressiveness" so to speak, it is too slow for my needs; the ML community gravitated towards that because it is simple to implement and test large models because C++/C/Cuda are doing the lifting. However, in my flavour/field of ML, python is often the bottleneck because the sampling/simulations are glued with it and the need to sample 10s of millions of steps ends up with possibly billions of function calls in python.

You might take a look at Julia with PyCall and RCall, if you haven't already. I find it solves a lot of headaches I have with Python while allowing me to leverage the ecosystem.
> There's a reason it took a decade to get projects onto python 3, but PHP can swiftly move the entire ecosystem from v5 to v7 in a year.

PHP did well with v5 to v7. It took more than a year but your point is well made.

The PHP v7 to v8 move is not as easy [1, 2, 3]. Not having Python 2 to 3 fresh in everyone's minds is one reason; not having failed PHP v6 is another. The breakages that led to the downfall of PHP6 made backwards compatibility a focus in PHP7.

Since PHP7 was released the contributors/voices who favour backwards compatibility have left or been drowned out. The emphasis on supporting legacy code has gone.

[1] https://stitcher.io/blog/new-in-php-8#breaking-changes

[2] https://24daysindecember.net/2020/12/21/a-perfect-storm/ (recommended reading)

[3] https://developer.yoast.com/blog/the-2020-wordpress-and-php-...

To take one of your complaints:

> Why do my lines of code need to fit on a punched card invented decades before the language was invented?

Keeping lines short helps keep information density high, allows you to fit more views of the code on screen without wrapping, and encourages use of intermediary variables to keep complexity per line low.

This isn't a Python issue. Any decent style guide will have a maximum line length.

> Python is a fractal of closed-mindedness

I don't know what you mean by a "fractal" of closed-mindedness, but you seem very certain of your opinions and not very open-minded.

Line length isn't a python issue. A line length of 80, is. (Thus the punch card reference).

My stint in python made me appreciate A line length more, but 80 got to be absurd.

What does a line length of 80 have to do with punch cards?

My understanding is that 80 was originally chosen because that was the standard number of columns on a terminal. It has been continued to be used because it happens to be roughly just under half the width of a typical 16:9 or 16:10 monitor at normal font sizes, making it ideal for window snapping and side-by-side diffs.

I have to review code in other languages all the time which follow a style guide that defines a maximum line length of 120 or 160. It's a huge pain having to scroll left and right all the time. But I'm not about to replace my perfectly good 16:10 monitors with ultrawides.

It's actually 79 chars.

> the standard number of columns on a terminal

are inherited from punched cards.

https://stackoverflow.com/a/4651037/257493

But in 2021 I can resize my TTY Emulator, we have better fonts, and screen proportions have changed.

A column length of 80 is in the ancestral DNA of computers because it was the number that packed nicely onto a sheet of paper. That paper was that size because it fit nicely into a paper portfolio and filing cabinet. The portfolio was that size because it was a reasonably big-enough sheet to adjust the paper cutter to.

80 has nothing to do with humans reading code. It's all post-hoc justifications and fairy tales. The whole thing is a 200 year old legacy system bug.

> we have better fonts, and screen proportions have changed.

I have two 24" 1920x1200 monitors which I develop with. Just three or four years ago I and most of my coworkers didn't even have that.

When I open VS Code in full screen using the default font size and two editors split vertically on my 24" monitor, I can fit 90 characters per line before I have to start scrolling horizontally. If I hide the side bar, I can fit 110 characters per line. If I reduce the font size to one which is barely tolerable, I can fit 114 characters per line with the side bar. I can finally fit a full 135 characters per line if I hide the side bar with the tiny font size.

If I snap VS Code to one half of my monitor with the default font and one editor and the side bar open, I can barely fit 80 characters per line. If I close the side bar, I can fit 110 characters per line.

If I open a JetBrains IDE using the default font size, no side bar, and two editors split vertically, I can fit 107 characters per line before I have to scroll. If I do a full screen side-by-side diff with the IDE, I can fit 108 characters per line on each side.

If I open a side-by-side diff in Bitbucket in a full screen browser at the default zoom level, I can fit about 85 characters per line on each side with the side bar open. If I collapse the side bar, I can fit about 108 characters per line on each side.

Of the environments I work in every day, the only ones which support 120 characters per line with comfortable font sizes and without consuming more than half the screen are Beyond Compare and Vim. Most of them don't even support 100 characters per line without making compromises and none of them support 160 characters per line.

Human brains are the same or worse (smaller) for 100_000 years or more.

What do you think is easier to read: one long line or the same text in columns with a suitable width? What do you think this width would be? The optimal width may depend on the language e.g., I'd use >1.5x for Java, to get the same information density per line as in Python.

Your eyes/working memory don't scale with the width of your monitor.

PEP8 (the generally-used Python style guide for any non-Python people reading along) defaults to 79 characters, with an option to increase it to 99.

Personally I'm fine with either option, but I think lines much longer than 99 characters start to get pretty awkward. If you really do need longer lines than that occasionally you can mark an exception for whatever style checker you're using.

Well said. Also python doesn't enforce this at all. You can use camel case everywhere and have 149 columns if that's your thing. Perhaps the other kids won't play with you, but that isn't a fault of python.
To me this sounds like a big straw man of a "the python community" that does not exist in that form. Things like the packaging system being shortcomings is widely acknowledged, I've very rarely encountered people being dogmatic about "pythonic" code, especially when it just comes to style guide. You've encountered one or two, and are extrapolating to an entire community about that.
You berate someone for limiting his post to his context and do the exact same thing yourself.

> I've very rarely encountered people being dogmatic about "pythonic" code,

Followed by...

> You've encountered one or two, and are extrapolating to an entire community about that.

I'll have to say my experience in "the community" has been far closer to his experiences than your equally small bubble.

Whatever language you're using, someone hates. Just be aware of that. There is no perfect language that fits every situation and there is no perfect community.

Know your needs and which language fits your needs.

I can't disagree. With one exception, that being Raymond Hettinger, it's to the point of cringiness when someone says "pythonic". The last 2 years of my last job was 99% python, and the smug, "we love what we did and nothing else" attitude I think you've exactly captured.

I know this is not universal, but I've seen it online a lot, and lived it for far too long.

If this article was about the WTFs in PHP there would already be hundreds of comments saying how much it sucks.

You get less criticism about other languages although they also have bad design decisions (which is totally fine since nothing is perfect).

I wish these things weren't so biased by the emotional investment people put in working tools, which is all a language is.

Compared to wtfjs, I'd say this is pretty tame.

The only one I actually encountered myself is `def some_func(default_arg=[]):` one, but it was indeed a very "WTF" moment.

I’ve seen this in large (open source) code bases.

The only one that has really bitten me was implicit string concatenation when forgetting a comma in a list literal.

Tips on learning python as a seasoned dev?

I will start a new job where everything is done in Python in a few weeks. Learning the syntax of the language is easy, but what about "everything else" using a language entails? Like how is stuff written in practice (pythonic / idiomatic), build tools, libs everyone use, workflow, etc

I'm not a python developer by trade, but here are some of the resources I have found helpful. The links below are in the order I would read them if I was starting again.

Hitchhiker's Guide to Python - https://docs.python-guide.org/

* Setting up virtual environments

* Structuring projects

* Code style (PEP8)

* Popular libraries for various use cases (i.e. Django/Flask for web applications)

setup.py (for humans) - https://github.com/navdeep-G/setup.py

* Instructs on how to write a setup.py file for distributing python packages

* Contains links to other resources to learn more about setup.py

Python 3 in One Picture - http://coodict.github.io/python3-in-one-pic/

* I sometimes forget syntax when switching between different languages :)

A Guide to Python's Magic Methods - https://rszalski.github.io/magicmethods/

* The special methods typically used with object oriented python that start and end with two underscores "__"

Awesome Python - https://github.com/vinta/awesome-python

* In their own words "A curated list of awesome Python frameworks, libraries, software and resources."

* Whenever I start on a new language or skill, I always check if there is an "awesome" list available

Hope you find these as useful as I have!

My biggest issue with Python that is not related to syntax is that it isolates you from the Unix machine in a bad way.

I get that they try to support Windows with the same abstractions, but they are all leaky and subtly broken (like shutil). If you use Python too much, you lose the understanding of Unix.

Other languages aren't like that, for example Perl and Go.

Whenever I stop using Python for 2 weeks and focus on C or Go, my understanding of the machine and what is actually going on increases dramatically. I've even considered learning Perl, but haven't done it yet due to lack of time.

There's always Ruby - best bits of Perl, Lisp and Smalltalk whilst retaining UNIX heritage.

    >>> a := "wtf_walrus"
    File "<stdin>", line 1
        a := "wtf_walrus"
          ^
    SyntaxError: invalid syntax
Why doesn't this work? Isn't the walrus operator effectively just an alternative to the assignment operator which also returns the assigned value? Why specifically make it fail in this situation?
From the PEP [1]

> This rule is included to simplify the choice for the user between an assignment statement and an assignment expression -- there is no syntactic position where both are valid.

1: https://www.python.org/dev/peps/pep-0572/

Ah. I forgot about the "one right way" philosophy.

But if there is no syntactic position where both are valid, then why not just add a return value to the existing assignment operator? Surely that wouldn't cause any compatibility issues since prior to its introduction any attempt to use an assignment as part of an expression would raise a syntax error.

Looks like in C++, where the maintainers don't dare to say no to add simple syntax to win popularity contests, but hereby completely destroy the language. JavaScript also comes to my mind.

walrus, haha

  # Python version 3.8+

  >>> a = 6, 9
  >>> a
  (6, 9)

  >>> (a := 6, 9)
  (6, 9)
  >>> a
  6

  >>> a, b = 6, 9 # Typical unpacking
  >>> a, b
  (6, 9)
  >>> (a, b = 16, 19) # Oops
    File "<stdin>", line 1
      (a, b = 6, 9)
           ^
  SyntaxError: invalid syntax

  >>> (a, b := 16, 19) # This prints out a weird 3-tuple
  (6, 16, 19)

  >>> a # a is still unchanged?
  6

  >>> b
  16
In what way is ANY of this unexpected?

For starters, unpacking is not the same as assigning inside a tuple...

(And how is the 3-tuple "weird"? It prints exactly what you did...)