
WTFPython – Understanding Python through surprising snippets - throwaway_shame
https://github.com/satwikkansal/wtfpython/blob/master/README.md
======
jakear
My biggest complaint about Python is that it somehow doesn’t get flak for
having the same (if not worse) scoping as JS, which gets endless hate for its
function-scoped variables. (So much so that block scoped variables are the new
normal in JS, but not in Py!)

Take for instance:

>>> powers_of_x = [lambda x: x^i for i in range(10)]

>>> [f(2) for f in powers_of_x]

[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

To me this is more absurd than any of the JS “wat”s I’ve seen.

In JS:

powersOfX = Array.from({length: 10}).map((_,i) => x => x^i)

powersOfX.map(f => f(2))

Not a particular fan of the Array.from over list comprehensions in terms of
syntax, but I much prefer consistency of semantics JS provides by not adding
new syntax.

Here Python has special syntax for both list comprehensions and for anonymous
single line functions, and they interact in a highly unexpected way.

Funny that Py doesn’t need semicolons, as I’m reminded of GJS telling me of
adding special forms to languages: “beware the allure of syntactic sugar, lest
you bring upon yourself the curse of the semicolon” (paraphrased of course, he
said it much better). It seems Python has fallen into the trap of syntactic
sugar, with the curse manifesting itself not in the form of a semicolon, but
in confusing and unexpected interactions between its various special forms.
Another example: the walrus operator and all its oddities listed in TFA.

~~~
BiteCode_dev
I agree totally with that, it's an ugly trap in python, although mitigated by
the fact the language doesn't encourage this kind of paradigm so you rarely
encounter it.

But more absurd than creating a global variable if you forget "var/let/const"
? More absurd than "this" being schizophrenic ? More absurd than having no
namespace for 20 years ?

That's pushing it.

~~~
jakear
In my opinion, if standard tooling is able to identify the foot-gun, it’s not
too terrible of a foot-gun. I don’t think standard python tooling would catch
the above issues, but standard JS tooling would identify a missing variable
declaration. As for the other things you mention (this, namespaces), I never
said JS was perfect! Just that JS got a lot of shit for its scoping, fixed the
problem, and now has sane scopes. Py has scoping with a lot of the same
problems JS used to have, but for some reason didn’t fix them, and thus to
this day is riddled with these foot-guns.

~~~
BiteCode_dev
> I don’t think standard python tooling would catch the above issues,

Of course it does.

    
    
        $ echo "
        > powers_of_x = [lambda x: x^i for i in range(10)]
        > [f(2) for f in powers_of_x]
        > " > test.py
        $ pylint test.py
        ...
        test.py:2:27: W0640: Cell variable i defined in loop (cell-var-from-loop)
        ...
    

Pylint is the most powerful linter for Python is well integrated in VSCode or
PyCharm.

When concerns are raised about JS, people are quick to answer you are not
supposed to use it without modern tooling (Webpack + babel + typescript +
eslint + lowdash...), although many devs still do.

Now, I use and teach a lot of JS, and I enjoy many of its modern features. I
especially like the object destructuring, it's even better than Python's
unpacking.

But objectively, you can get a very decent experience in Python without any
kind of tooling. And yet, there is a lot of it if you want to up your game:
pylint, black, mypy, jedi, pytest, poetry, etc.

~~~
jakear
That’s good to hear. As I said, I don’t use python much anymore.

------
gonational
I went to the page thinking, “I’m sure these will be the usual gotchas.”

OMG was I wrong...

Some of the examples are unbelievably weird and completely counter-intuitive.

------
BiteCode_dev
It's a good repo, but remember a lot of those are "A Good Thing™".

The first snippet is a very good example:

    
    
        a := "wtf_walrus"
    

doesn't work while:

    
    
       (a := "wtf_walrus")
    

works.

It's a fantastic design decision.

Python took a long time before getting this operator, because it's a language
that favors being readable, easy to use, and above all, to learn.

But in many other languages, the very same operator is often misused as, or
confused for, the operator for equality or assignation.

We, as a community, didn't want people to wonder why there are several ways to
do assignation (we have this problem with string templating already). Instead,
we wanted to be sure that people could ignore the existence of ":=" for some
times during their learning process.

And so the decision has been taken to make is very easy to distinguish it from
"=" and "==", forcing parenthesis when necessary to make it clear this is a
completely different use case. Also to hint people at using it only when
necessary.

If you like scripting in Python because it's so easy to go from an idea to
code, it's not random luck. It's because the language is a collections of
thousands of such decisions.

~~~
fxj
in the zen of python (import this) it says:

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

sadly this is not true for a while in python now. Python became a language
that can be really hard to read now.

~~~
michaelcampbell
As a relative newcomer to Python (compared to most, but I do use it
professionally every working day for a year, and some non-working days for
personal stuff), I find this to be the case. At work I have to use Django, and
it's almost a chore to find anything explicit or obvious in it. Its level of
abstraction is "Too Damn High" as the meme goes, IMO.

It feels like saving boilerplate for the sake of it which just makes obvious,
readable code into magic incantations where shit happens and you don't know
why or how or where to look.

~~~
nickserv
This is the danger with high level frameworks, everything is abstracted away,
and with a dynamic language like python the black magic can go very deep.

If you've ever taken a look at something like Flask or Cherrypi, the
difference is very apparent.

That said, I've been doing some Rails lately, after years of Django, and I'm
finding the experience to be worse from a readability point of view.
Metaprogramming everywhere, modifications of language constructs ... It gets
very confusing very quickly. Could be simply a lack of experience of course.

In parallel, I've also been using Go and the experience could not be any
different. So much easier to understand what is going on, but so much
boilerplate code!

It doesn't look like it's possible to have both ease of use and
explicitness...

~~~
michaelcampbell
Thanks; I've glanced at Flask and it makes a lot more sense to me, but this is
a project of immense size and age, and not under my control, so I just have to
live with it. Overabstraction for the sake of it seems to be Django's mantra.
Then there's Django Rest Framework, and the 3-4 different filtering
mechanisms.

It's really a nuisance.

------
varelaz
Probably the best collection/explanation of edge cases which are not a bug and
supposed to work that way by design that I ever have seen for Python. (it's my
pramary language for almost 10 years).

------
erezsh
This list is a little nit-picky.

All the id() equivalencies only exist for optimization, and not out of some
promise to the user.

The walrus operator can be confusing, but any language can be made to look
confusing. It's an advanced feature that should be used very scarcely (if at
all).

It does point out a few real weak points in Python, but most of them require
non-trivial usage of the language.

Meanwhile, in Javascript

    
    
        > "a" + undefined
        "aundefined"
    

Now that's what real WTF looks like.

------
baq
the first really wtf thing on there is
[https://github.com/satwikkansal/wtfpython/blob/master/README...](https://github.com/satwikkansal/wtfpython/blob/master/README.md#-mutating-
the-immutable) where both effect and exception happen; up to that point it's
business as usual with garden variety corner cases or implementation details.

~~~
avip
I find the explanation lacking? _sort_ also "changes the list in-place", but
you can do _([3, 1, 2],)[0].sort()_

~~~
Luyt
Of course you can. The first element of that tuple is a list, which is
mutable, and can be sorted in-place.

~~~
FartyMcFarter
So the question is if += and sort are both "in-place", why does one throw and
the other doesn't? Clearly "in-place" is not the full explanation here.

I think the real explanation is that with += there are two steps:

1- The existing list gets modified. This is fine since lists are mutable.

2- The tuple's reference to the list gets updated (even though this update is
unnecessary since the list object's identity is the same).

The exception occurs at step 2, but this step is otherwise a no-op. Whether
mutating a reference to the same value it already had is an actual mutation is
an interesting semantics debate.

~~~
Luyt
Tuples are immutable, but that doesn't mean they can't contain references to
mutable objects.

When you want to use a tuple as a key in a dictionary, Python checks if all
members of the tuple are immutable. If one or more of them aren't, you'll get
an 'unhashable type' error.

------
rvanlaar
Python 3 supports typing. The integration with VSCode is also nice.

The following is valid typed code and my personal favorite WTF:

    
    
      def func() -> int:
          return True
    

Because:
[https://github.com/satwikkansal/wtfpython/blob/master/README...](https://github.com/satwikkansal/wtfpython/blob/master/README.md#-whats-
wrong-with-booleans)

I understand the decision that was made in the python 2 era. Nowadays Boolean
shouldn't be a subtype of `int`. It goes against the zen of python:

\- Explicit is better than implicit.

It actually happened because I was using the new walrus operator (which I
like).

    
    
      def calc(input: int) -> int:
          return input // 3
    
      def func(input: int) -> Iterator[int]:
         while input:= calc(input) > 0:
             yield input
    

*edit: code layout

~~~
d33
Yeah, typing KINDA works until you try to use a library that has no
annotations and uses reflections, such as - say - plumbum, boto3 or some other
pre-typing shit. It's a bit like with async code - once you decide to use it,
you can keep the language but basically need a whole new ecosystem. I tried to
write an actual company project with `mypy --strict` passing as a requirement
and you quickly end up having abstractions only to bypass mypy, as well as
surpress comments as uses of Any. And this is where it pays off to just switch
to a statically typedk, compiled language.

~~~
rvanlaar
Yeah, agreeing with what you say.

mypy --strict is only useful for projects for which everything is defined.

I recently did a project with tornado, which is typed. Typing helped me
immensely. My editor would give me type hints, errors and show me other edge
cases.

For an existing Django project I'm adding types when I touch functions. It
helps a bit, but way less. That's also because typing almost forces you to
change all the dicts that get's passed around in dataclasses. Which is a lot
of work after in an existing project.

------
blackhaz
It's interesting to see the walrus operator in there. I wonder, is this
another example of unnecessary features being added just to claim a bigger
change list, or someone really needs this operator? Why would anyone prefer a
walrus instead of adding just one more line to their code, which also helps
with readability?

~~~
Luyt
A while ago, I was also sceptical about the walrus operator, although I use
assignments in expressions all the time in C++. But when I was perusing a
library written Python the other day, I found five spots in which the walrus
operator would make sense - eliminating one line of source code without
readability suffering from it.

~~~
bakery2k
Is saving 5 lines of code really worth breaking Python's "one way to do it"
design principle?

~~~
CallocRoast
That principle was bullshit anyway. There have always been multiple ways to do
things, the one way was just whatever the elder Pythonista deemed to be
pythonic that day.

~~~
aaronchall
Not true. The language optimizes for the way it is intended to be used, and
changes remain sensitive to those optimizations.

"Pythonic" means intended usage, and "unPythonic" is shorthand for "you found
another way to do it that kinda does what you want but (is ten times as
slow/takes up ten times as much memory/doesn't work for edge-cases/has more
unintentional side-effects) because it wasn't the intended usage, which is
fine for your own personal projects, but please don't bring that into my code
base, and pretty please don't teach other people to do it that way..."

------
mesaframe
Golden repo.

I remember reading this a couple years ago and it opened my eyes to things
that I was oblivious to.

------
kraf
Maybe "beautifully designed" is a bit much then

In my experience Python is not very nice to work with. I do like the ecosystem
for data science though, it's just amazing how much there is. Hopefully the
language will grow into something better now that the dictator stepped down.

~~~
CallocRoast
Second this. I can’t understand finding Python beautiful or even elegant. It
just took a hodgepodge of features from languages like Haskell and C++, and
repackaged them clumsily. It’s good for writing short scripts and throw-away
code, but it shouldn’t be used as a serious programming language.

------
ngcc_hk
This is crazy.

------
NotSammyHagar
If only it had curly braces instead of just indention. God, that kills me. And
used unicode instead of ascii as the default. And there wasn't python 2.7 vs
3. Someone help me stop this list.

~~~
baq
1\. forget python 2.7 exists

2\. can't help with the whitespace, sorry; also take care handling Makefiles

~~~
Erlich_Bachman
> 1\. forget python 2.7 exists

But it doesn't work this way. You don't work strictly with the language
itself, you work with the whole ecosystem. Libraries, tools, snippets, SO
questions, etc. And many of those are still in 2.7 or at least need to specify
different solutions for 2.7... It is still a pain.

~~~
protomikron
> Libraries, tools, snippets, SO questions, etc. And many of those are still
> in 2.7 or at least need to specify different solutions for 2.7... It is
> still a pain.

Admittedly there were some libraries that took a long time to switch, but at
_this_ point there is no serious library left, that did not make the switch.

~~~
dr_zoidberg
Exactly. I've gone farther even, and this is my motto now:

    
    
        Python 3.6+ or burn it to the ground
    

I mself still have to port a few projects yet, but those that got stuck in
2.7-3.5 land are en route to dying a fiery death, and being reborn in 3.6+
(3.8 where applicable, 3.6 as the lowest I'm willing to accept).

