WTFPython – Understanding Python through surprising snippets
github.com
github.com
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.
>>> powers_of_x = [(lambda i: (lambda x: x**i))(i) for i in range(10)]
>>> [f(2) for f in powers_of_x]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
Or you can use default parameter values to use a single lambda (though this means it can be overridden, it's not semantically equivalent to the js implementation) >>> powers_of_x = [lambda x, i=i: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
your python snippet is equivalent to the following JS: >>> function* comprehension(iterable){
>>> let i;
>>> for(let j of iterable){
>>> i = j;
>>> yield x => x**i;
>>> }
>>> }
>>> powersOfX = [...comprehension([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])];
>>> powersOfX.map(f => f(2))
[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]
And Python's comprehensions are actually a shorthand for writing the above generator with this syntax: yieldExpression for i in iterable. The semantics are consistent with the regular for..in >>> def comprehension(iterable):
>>> for i in iterable:
>>> yield lambda x: x**i
>>> [*comprehension(range(10))]
is equivalent to >>> [lambda x: x**i for i in range(10)]
This is all a consequence of python's late binding.The lambda case here is more like a default argument of a constant integer (immutable) than a default that is a list, which can be altered directly by the function it's a default of.
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.
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.
Even if you got the binding right, your powers of 2 would be shot down for being unpythonic anyway due to lambdas being shunned when there are cleaner ways to write things.
powers_of_x makes no sense as a variable name for a list of functions that you have to call in order with the same number to actually get the powers of that number. I get that it’s just a contrived example, but most can be turned into someone more apparent to the reader.
def powers_of_x(x, limit=10):
for i in range(limit):
yield x^i
Less memory, no anonymous functions, more flexibility, testable...IMO JS as a language is in a much better place than Python currently. I end up frustrated a fair amount by arcane errors in Python due to less expressive constructs or some wtf issues like unexpected variable scoping within Python modules.
I started to have fun coding in JS the day I accepted it was not Python and that I had to structure and style my code differently.
E.G: in Python you will use iteration a lot. A lot of a lot. But not so many callbacks. The reverse is true in JS.
But even without that, I'm not saying they are comparable. I'm saying the same API will use explicit anonymous callbacks in JS and something else in Python (decorators, subclassing, protocols, generators...). I'm saying that the same API will use __iter__ in Python and something else in JS (type conversion, proxy object, explicit method call...).
E.G, this is a Python pattern you'll find in contextlib or in pytest fixtures:
@somekindofregistration
def foo():
print('code that runs before')
try:
yield
except Stuff:
print('Error handling')
print('code that runs after')
This uses Python iteration mechanism to run code at 3 different times in a life cycle.While in JS, you would pass 3 callbacks.
You are misusing the word callback. A callback is a function passed to another thread that will maybe be invoked later as a response (like it calls you back).
Even MDN uses the term callback to describe e.g. the argument to 'map': https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
In contrast, Lodash goes out of its way not to use the term "callback", presumably to avoid confusion since it's a place where many functions-that-take-a-function-argument do not call the provided function, e.g. https://lodash.com/docs/#curry
>>> (1, 2) == (1, 2)
True
>>> (2, 1) == (1, 2)
False
If you need identity for perf: >>> (1, 2) is (1, 2)
False
If you just need the type check: >>> isinstance((1, 2), type((1, 2)))
True
It's very explicit, practical, and you can set the scale of practicality vs performances where you want. Plus: no implicit weird type conversion, only one equality comparison operator, and no hidden rules.I think it's sane.
This. I've made this mistake exactly once in the last 7 years, but that one time almost caused a security issue.
https://docs.python.org/3.8/faq/programming.html#why-do-lamb...
"This happens because i is not local to the lambdas, but is defined in the outer scope, and it is accessed when the lambda is called — not when it is defined. At the end of the loop, the value of i is 4, so all the functions now return 42, i.e. 16."
These sort of scoping gotchas are pretty common across all programming languages and are a great argument for unit testing. As usual the answer is be more explicit what you're asking the language to do:
>>> powers_of_x = [lambda x, i=i: x*i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Note the positional argument with default "i=i."
More discussion:
https://stackoverflow.com/questions/452610/how-do-i-create-a...
It's less of a problem in Python because it has built-in module support. A global variable is only global in the module, unless you import it with "from foo import *", which lazy programmers do but is discouraged by style guides.
for i in range(10):
def mul(x):
return i^x
powers_of_x.append(mul)
That to me coming from a JS background is totally wild for(var i=0; i<10; ++i) {
powers_of_x.push((x) => i^x);
}
behaves the exact same way for the same reason?By definition, a closures closes over its definition context, it doesn't capture the values at definition time but instead keeps referring to said definition context. If the definition context is mutable and modified, the closure reflects that change when it's finally invoked.
for(var i=0; i<10; ++i) {
powersOfX.push(x => x^i);
}
will behave the same way because it does the same thing, so will e.g. for i := 0; i<10; i++ {
powers_of_x = append(powers_of_x, func(x int) int { return x ^ i })
}
in Go.One of the confounding factors is that many languages have block-scoped for loops especially when using iterators instead of low-level C-style loops; that's also the case in Javascript when using `let` bindings (and is in fact one of the major reason to use `let` instead of `var` if that's possible).
The underlying concern is still there[1], but this very common failure case is avoided: rather than update a single binding, each iteration creates a brand new binding for the closure to close over.
Alternatively, a common mitigation technique is to emulate that using e.g. immediately invoked function expressions.
In Python you can also use the "default parameter" trick to shadow the closed-over binding (though most of the time this is used for performance reasons) without the overhead (both syntactic and runtime) of lambdas in lambdas in lambdas:
for i in range(10):
powers_of_x.append(lambda x, i=i: x^i)
[0] excluding the special case of low-level languages with capture clauses, as well as languages like Java where closing over mutable bindings is specifically forbidden (a lambda or anonymous class can only close over `final` bindings)[1] and remains a regular issue in async code fighting over closed-over context
std::vector<std::function<int(int)>> powers_of_x;
for (int i = 0; i < 10; ++1)
power_of_x.push_back([=](int x) { return std::pow(x, i); });
works as one would intuitively expect (a different i is captured for each iteration). The issue is with those languages that conflate values with references.> excluding the special case of low-level languages with capture clauses
Of course it works, you're specifically capturing `i` by value. It's not exactly surprising that doing things completely differently yields a different result.
Also, python could have chosen a slightly different closure semantics and preserved sanity: instead of closing over the binding itself, it could close over each object reference separately (exactly in the same way the default parameter hack works).
https://play.golang.org/p/pn2jBTaTNS8
package main
import (
"fmt"
)
// return a^n
func Power(a, n int) int {
var i, result int
result = 1
for i = 0; i < n; i++ {
result *= a
}
return result
}
func main() {
fmt.Println("Hello, playground")
x := 2
var powers_of_x []int
for i := 0; i < 10; i++ {
powers_of_x = append(powers_of_x, func(x int) int { return Power(x, i) }(x))
}
fmt.Println(powers_of_x)
}
//Output:
//Hello, playground
//[1 2 4 8 16 32 64 128 256 512]1. uses a slice of integers which completely misses the point
2. uses an IIFE which completely misses the point
The point is to look at what happens when you close over the iteration variable.
GJS: Gerry Sussman attributes this quote to Alan Perlis.
_Structure and Interpretation of Computer Programs_: https://mitpress.mit.edu/sites/default/files/sicp/full-text/...
/s
x ** y a = 0
def f():
print(a)
a = 1
f()
f() a = 0
def f():
print(a)
f()
Maybe this is all obvious to you, but I'll bet you can't name any other language which behaves this way. Compare the original Python to JavaScript (or Lua, Perl, Tcl, Ruby, C, Scheme, Rust, Clojure, or ...): a = 0
function f() {
console.log(a)
a = 1
}
f()
f()
This one behaves how I think most people would initially expect the Python snippet to behave. First it prints 0, then it prints 1. (Of course JavaScript has it's flaws too...) If you don't believe me, ask your coworkers and friends what they think it does before running it.My only real point is that Python conflates variable declaration and variable assignment in a way which initially seems like a friendly time-saver, but which ends up being pretty subtle and confusing until you've learned its quirks. All of that just to avoid declaring your variables (in some fictional version of Python):
var a = 0
def f():
print(a)
a = 1
f()
f()
Here it would be clear which are declarations + initializations, and which are only assignments. And for just the cost of typing the word "var", the compiler could tell you when you've made typos in your variable names. As an added bonus, you could get rid of the "global" and "nonlocal" keywords.But at least I like the idea that it gives an error message when confronted with ambiguity. That way it doesn't do something you didn't expect silently. Sadly it only gives it at runtime, not at compile time.
However, there are plenty of cases where Python won't give an error message too. Make a typo somewhere in the middle of your function, and it'll quietly introduce a new variable instead of letting you know. A rarely used branch of an if-statement could hide this indefinitely. This could also be avoided (or at least mitigated) by requiring variables to be declared explicitly.
var a = 9;
function foo() {
console.log(a);
var a = 12;
}
foo();
$node foo.js
undefined
The same goes with ruby.OMG was I wrong...
Some of the examples are unbelievably weird and completely counter-intuitive.
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.
Instead, they’ve chosen the narrative: “the walrus operator is a lot like the assignment operator, but it is able to be used in expressions, cannot be used as a statement, and it sits below the comma operator in precedence instead of above it. We understand that this is super confusing, so use it sparingly”.
One way you have two operators for people to learn, with the understanding that there is a third legacy operator that works pretty much the same as one of them. The way they chose you have three operators for people to learn, with two of them behaving pretty similarly but not interchangeably, and with subtle yet important differences.
Because we have experience with things such as easy of learning, cognitive load, writing simple to read code, making it hard to introduce bugs...
This comment show how little experience one can have with it, and yet make a quick judgment to offer to "fix" things. E.G:
> eel free to use it in all places you’d previously use the assignment operator.”. Simple, easy to understand, easy to learn, and most importantly (though python folks might disagree, given 2 => 3), easy to adopt.
Well because in many languages, people do this:
while (foo = bar):
While they mean this: while (foo == bar):
Even experienced devs do this by mistake from time to time. It's hard for tooling to find out if you are doing something stupid or smart. Beside, you don't use tooling when you learn the language or do a quick script.Designing a language is not a 15 days job in a rush. Usually.
To reiterate, my proposal is that the walrus operator has the exact same semantics as the assignment operator, but creates an expression rather than a statement. A linter rule could then be added prohibits the assignment operator, and converts all instances of it to the walrus operator. The codebase now has only the two operators. Language reference books need only explain the two operators, and simply note that the legacy assignment operator behaves the same as the walrus operator but cannot be used in expressions. This is easy to learn.
What the language creators have done instead is add a whole new operator with all new semantics, leading to all new sources of confusion. (See TFA). This is not easy to learn.
Side note: It’s funny that you appeal to the authority of the language creators with phrases like: Python is easy because “the language is a collections of thousands of such decisions”, yet ignore the fact that the actual creator of the language was so against the addition of this operator that he saw the community’s insistence on it as reason to step down as BDFL.
Granted, I apologize. It was childish.
> but creates an expression rather than a statement.
Expressions are limited in Python for the same reasons. It's the same rational that got us tone done lambdas. People code with a certain style using it, which is not the style we want to promote for Python.
> A linter rule could then be added prohibits the assignment operator, and converts all instances of it to the walrus operator.
Every time you delegate something to a linter, you fail. A good language must be a good language without 3rd party tooling. Tooling should only be a bonus. This is why we have forced indentation in Python. This is why we have namespaces and don't rely on a bundler.
> What the language creators have done instead is add a whole new operator with all new semantics, leading to all new sources of confusion. (See TFA). This is not easy to learn.
You don't learn this operator when you learn Python. Just like people don't learn about list comprehensions at first, don't use yield and code without creating a decorators for years.
Python scales down: you are productive with it without all its features. It is designed to be partially learned and still be useful. Thousands of Python devs are not coders, but scientists, geographers, finance people, etc. They know very little Python and yet, can work all day with it.
Walrus is made so that it doesn't interfere with the original design. It's something separate that you get to later.
> yet ignore the fact that the actual creator of the language was so against the addition of this operator that he saw the community’s insistence on it as reason to step down as BDFL.
That's not what happened. The bad quality of the debate is what made Guido step down. We had a huge number of new comers in the later years because of Python popularity. These people didn't learn internet from the age of mailing list, IRC, etc. They addressed the topic like others talk on Twitter: lot of noise, little content. I understand that after 30 years of doing what is basically free awesome work for thousands of people, he didn't feel like being disrespected by a horde of juniors that though they knew better.
Regarding this part, Guido specifically addressed the toxic debate as the reason for stepping down as BDFL, and not the operator per se.
(Yes, I'm reinforcing BiteCode_dev's reply on this point)
Thank you.
That's backwards. The community was generally opposed to the walrus operator, and Guido stepped down because of (among other things) the community's reaction when he insisted on adding it.
Not because "Guido", I had the opposite reaction to asyncio, which he designed, enthusiastic at first then I began to see many design flaws.
But because it does make regex matching and reading bytes safely better.
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.
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.
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...
It's really a nuisance.
Yes, this has been one of the hardest balance to find. And believe me when I tell you the community tries very, very hard. But it's a difficult problem: making the language evolves fast enough, but keeping it solid and stable. Very difficult indeed.
> Python became a language that can be really hard to read now.
Not in my experience.
My job involve going a lot from company to company. I see a lot of code, from a lot of different people.
The way people write Python hasn't change much during the last 15 years.
In fact, I'd say because of Python 3, there is less cruft all in all.
You do have a few heavy stuff. E.G: asyncio or the type hints. But how many code out there uses that ? Very little. Mostly, the code that needs it.
Which is good. This is what we want.
e.g.
# compute pi
1000000 >> ψ( ψ(χ>>op("(x**2+y**2)**0.5<1")@rlµ<<χ)>>Σ*4>> _/_)
or this: # 10 fibonacci numbers
[x:=[1,1]] + [x := [x[1], sum(x)] for i in range(10)]
is valid python code. (for the first see my github jamitzky/iverson).
Not that I wouldnt like it, but python3 has changed and the zen of python is not valid for some time now.E.G, calculating primes:
>>> print(list(filter(None,map(lambda y:y*__import__('functools').reduce(lambda x,y:x*y!=0, map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]
As for the unicode caracters as variables... Remember you could do that in Python 2 ? # -*- coding: rot13 -*-
cevag "Relax"
But people don't do it. Just like they don't use "import *" everywhere, or monkey patch methods like it's going out of style the way Ruby loved it 15 years ago.Because such capabilities are restricted (one line lambda, parenthesis for walrus, not all unicode is allowed as var names or everywhere...), introduced slowly, and the community culture is to value readability, Python stays Python.
I was against the walrus personally, for the reasons you mention. But while I do see Raymond Hettinger trolling twitter regularly with his latest crazy walrus magic, in production we see no such thing.
Not to say it never happens. It does, I've seen monstrosities in the field. Like with every tech. I mean, you can start a fire with a water hose if you try hard enough.
But it's rare.
let x = <val> in <expr>
into this: (lambda x: <expr>)(<val>)
(as the lambda calculus does)or this:
[<expr> for x in (<val>,)][0]
of course the readability is terrible, but the power was always there if you wanted to (ab)use it :)(yeah, i've used the second one in a repl, don't @ me)
Tried to run this in a REPL and got invalid syntax.
This is no surprise Guido evolved with it, and changed his mind about a few things.
People are never happy. On the other side of the fence, you have others crying the language is not moving fast enough.
This is why we had Python 3 in the first place. Because text handling was such a cause of pain.
This is why we have type hints in the first place. Because big projects using Python felt let down.
And Python 3 and type hints are also a huge source of criticism (I was the first one to do so).
It's never ending. People will complain. People will be unhappy. Things, also, will be imperfect, as we are limited in resources and are submitted to many constraints. Not to mention taste and opinion, that are used to request changes.
But I'm not answering to dismiss valid concerns. I'm answering because as a very experienced Python dev, I now have a good grasp of what is due to culture, lack of experience, or a real deal.
E.G:
- := forcing parenthesis. Has not been a real problem in my experience, at least as of today.
- Packaging still more complicated that necessary. A real problem in the field (although 50% of it is a teaching problem: https://twitter.com/bitecode_dev/status/1188130895653416966).
If only people cared for fellow human beings, or earth, as much as they care for esoteric Wulrus operations in this or that language.
Why big projects started using Python in the first place is a very good question, given that this language had not been designed for them...
And as far as I'm concerned, the walrus operator breaks it in no way whatsoever: an assignment statement is the one obvious way to perform an assignment, and it's a syntax error to use a walrus as an assignment statement.
1. manual string concatenation 2. the old, printf-style. Still heavily used in e.g. stdlib logging module. 3. .format() 4. f-strings 5. string.Template in the stdlib
Yup, I definitely see this as one of the bigger failures in Python's design.
Even in the p3 transition, the community screamed to not remove them. Did you know we removed % for a few versiond ? We had to put it back because of all the complaints.
> The very same operator is often misused as, or confused for, the operator for equality or assignation.
It will still be confused, I'm afraid. It's just a little less likely that that confusion will be silently accepted by the interpreter.
> Also to hint people at using it only when necessary.
Let's be clear: the walrus operator was never, ever, necessary. It's syntactic sugar to avoid extra lines of code. The hope is that it will be convenient and obvious enough that folks will prefer it to both extra lines of code and repeated sub-expressions, but the very advice that beginners ought to "ignore the existence of := for some time" (_plus_ the intentional deviation from other languages) suggests that this is not going to be nearly as obvious or convenient as its proponents might have wished.
How about this from the second example:
>>> a, b = "wtf!", "wtf!"
>>> a is b # All versions except 3.7.x
True
>>> a = "wtf!"; b = "wtf!"
>>> a is b # This will print True or False depending on where you're invoking it (python shell / ipython / as a script)
False
Or this from further down >>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
FalseAll 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.list.__iadd__ is (sadly) defined as something like
def __iadd__(self, other):
self.extend(other)
return self
So it's possible to have __iadd__ itself succeed modifying the list in place but then the "no-op" reassignment fail.[0] like many data model operations there's really a bunch of fallbacks depending on what is and is not implemented
It's not exactly a bug but it is a somewhat unexpected behaviour and IIRC Guido regretted that list.__iadd__ was overridden this way.
> None of the other += operators returns a value.
It's not the operator which returns a value, it's the data model hook. The data model requires that it return a value: https://docs.python.org/3/reference/datamodel.html?#object._...
> These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self).
Really the issue is that the "in-place" hooks are simply weird.
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.
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.
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...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 layoutmypy --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.
sum(is_true(data) for data in items)
to count how many items in an iterable satisfy a condition. sum(1 for data in items if is_true(data))> There should be one — and preferably only one — obvious way to do it.
Which is a very, very different assertion. And there remains one — and exactly only one — obvious way to do an assignment, because the walrus operator is literally invalid syntax as a statement:
>>> [a:=1]
[1]
>>> a:=1
File "<stdin>", line 1
a:=1
^
SyntaxError: invalid syntax"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..."
data = expensive_function(blah, blah_blah)
if data:
# many lines of processing
data = expensive_function(blah, blah_bah)
And seen a lot of times where newcomers forget the assingment at the end that makes everything move. So yeah, the walrus version would be a lot simpler: while data := expensive_function(blah, blah_blah):
# process data
This is just one of the "edge cases" where the walrus makes sense.I remember reading this a couple years ago and it opened my eyes to things that I was oblivious to.
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.
Given how long it took to transition to Python 3 and how painful the transition was, I'm not sure people will have the appetite or patience for this anytime soon.
If anything, the one thing I hate about Python3 is dynamic typing - inferring datatypes in function bodies is great but strongly typed (and _enforced_) parameters and return values would clean up a lot of the problems I have seen, and created in the past.
High level programming language is meant to communicate, not to dictate the computation. Python happens to be easy to understand by humans.
But to contradict your point, there's also a fair amount of Python which is a dictation - Guido even called his job role 'Benevolent dictator for life'. I really hated some of the guidelines as a new programmer but after dealing with them for some time I understand that consistency is far more important than preference in many/most/all cases.
I primarily work with C#, but I've gradually come to like the "K&R"/Javascript style (where the 1st brace is at the end of the first line), precisely because it helps condense code vertically.
2. can't help with the whitespace, sorry; also take care handling Makefiles
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.
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.
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).Makefiles are half the reason I enable visible whitespace in my editors.
e.g.
y=0
for i in range(10):
x=i+1
y=y+x*2
can be written as: [y:=0] + [[x:=i+1, y:y+x*2] for i in range(10)]
The square brackets are the new curly brackets.It's just that no one does it.
And as for curly braces, try typing:
python -c "from __future__ import braces"
In a terminal :)Does for python, too.
Internal encoding: what the strings are encoded in memory (latin1/UCS2/UCS4 in python)
Secondary buffer: python strings can be encoded twice. They can hold a secondary encoded version if themselves as utf-8.
I must say it is a rather elegant way, but it still fits firmly within "a new way or inefficient code".