New Ways to Be Told That Your Python Code Is Bad
nickdrozd.github.io
nickdrozd.github.io
I stopped using ternary expressions because someone told me not to. I could start using them again, whatever.
I use while loops when the burden of describing the loop as an iteration is too high (too much of a stretch), or when I'm writing something that really isn't a composition of forEach/map/filter/reduce (gasp).
For me, it's whatever I need to do to get through code review without arguing too much.
Don't add linter checks for these things, it's condescending.
At the very least, have the "greater good" on your side, e.g. "it confuses some people!" or "it is often a bug!" or "it can become a maintenance nightmare!" or whatever. If your reason is "I like it better this way," well then yeah, the reaction I would anticipate is "who asked you?"
The reason this bothers me isn't that I strongly disagree, but that I'm in the habit of doing whatever my linter tells me, and adding stuff like this cheapens the thing and makes it harder to sell to "I'm an expert, get off my lawn" people.
Why am I even writing this, I'm knee deep in C++ these days.
The language quibblers of the programming industry have installed a set of opinions, not knowledge.
They proceed to waste time and energy with their assertions.
Pray they don't install another one.
I'm most likely less experiment than some people here but after 10+ years of writing code, I came to the conclusion that the best code is the most readable code.
If all developers could write code that is easily readable for them (even after a few months break in the project), the software engineering world would be a lot better :)
Of course it makes sense to avoid anti-patterns and bottlenecks, but no much more really.
It comes up a lot in discussions about Go. A simple, "readable" language in which I can't tell you at a glance what a block of code does or is responsible for.
I agree with this. It takes a lot of context to truly understand another's code so reviews are just smoke & style checks, not real logic scrutiny like a kernel dev might do - in most cases businesses aren't willing to accept the true cost of such a review; I think the same is true of KT.
> Don't add linter checks for these things, it's condescending.
disagree. wrt pep-8 stuff, devs should be using their own linters before code is properly committed and for review. CI checks are easy to add and manage a quick/easy rebuke for the dev that doesn't bother.
For other style lints, tools that standardise trivial things (style w/ black, orderings w/ isort) reduce arguments and establish a consistent style. Code in whatever style you like, just standardise it for consumption. devs then argue at a higher level instead: over linter parameters.
Well as he states, these checks are not turned on by default. I have trouble seeing why someone who liked these extensions enough to turn them on manually would feel condescended to by them.
It still puts me off slightly that the tool mixes "make your code less prone to error" with "this guy wants this." Ok, live and let live.
I'm reminded of Douglas Crockfords's jslint, which is a mix of "this caused bugs before" with "how I write javascript."
Consistent style is really nice.
The things that really impact how understandable some code is have little to do with punctuation, or whether or not these two characters have a space in-between, or what type of quotes we use.
> Generally speaking, less code is better than more code
This is true, but, generally speaking, readable code is better than compact code. Making it harder to scan through the code, or requiring more horizontal reading to understand code while you're scanning through it, is objectively bad.
This is going to push people towards making less-readable code for literally zero benefit. Considering how many of the checks in pylint are to make your code more readable, this is a clear step backwards.
I agree with him about the problems involved in while loops; there are cases where what you actually want is "loop forever until you die", but they are pretty rare, and all the rest can be converted into some other form of loop; that said, those situations also seem to more likely complicate the understanding of your code in many situations.
All in all, while-used is highly opinionated but has a point to make; consider-ternary-expression is an asinine addition that's going to push people towards writing objectively worse code for literally no benefit other than reducing line count. If that's your KPI, then sure, fill your boots, but it makes zero sense to enable it by default.
Edit: To clarify, it does not seem as though PyLint is enabling this by default, but I'm concerned that some shops will enable it by default and push their developers to write worse code.
I am concerned about the trend in Python open-source projects to consider any deviation from their machine-checked coding style a bug. There's pylint and pep8 and isort and 'black' - the list grows ever longer.
I kinda like it when people yell at me for submitting a patch without tests. Having quality standards is great. I like it a lot less when some CI machine yells at me for failing to put the "correct" number of blanks lines between methods.
This seems like a horrible attitude, if everyone did this codebase will go downhill really fast. Sometimes you have to use better/new practices and successfully advocate for them in code review.
> Don't add linter checks for these things, it's condescending.
Automated standardized checks are "condescending"?
> but that I'm in the habit of doing whatever my linter tells me
Sounds like you need to improve your lint checks. You _should_ usually be accepting the vast majority of lint auto fixes.
Because the Python ternary operator is fucking backwards! Why on earth would you put the condition in the middle!?
x = 4 if condition() else 5
vs.: condition() ? 4 : 5
vs. the author's own lisp example: (setq x (if (condition) 4 5))
I love ternary operators, and Lisp/ML-style if else blocks that return things, but Python puts the 4 and the 5 so far away from each other that even I hate using it, especially when condition is a chained mess rather than just a function call. c = condA ? valA :
condB ? valB :
defaultVal;PHP in all its glory and splendor actually originally used left-to-right instead of right-to-left associativity for the ternary ?: operator, so that expression you wrote works differently in PHP and (JavaScript or C or any other language). (Side note: notice how I used parens instead of depending on you guessing about the relative precedence of "and" and "or" in English.)
It's easy (and perfectly valid) to blame the designers of PHP for not knowing or caring or paying much attention to the syntax of the programming languages they were imitating, and doing something that stunningly stupid, but it just goes to prove that many people (especially PHP designers like Rasmus "I don't care about this crap at all" Lerdorf and typical PHP programmers) don't even have the faintest idea what the precedence and associativity of many of their favorite programming language's operators are, or what those terms even mean, and they just willy nilly cargo cult copy and paste and translate between PHP and JavaScript code examples from Stackoverflow, introducing terrible hard-to-spot bugs.
https://en.wikiquote.org/wiki/Rasmus_Lerdorf
>"We have things like protected properties. We have abstract methods. We have all this stuff that your computer science teacher told you you should be using. I don't care about this crap at all." -Rasmus Lerdorf
But the real fault lies not just with PHP for parroting C incorrectly, but with C for doing something that stupid and hard to remember and easy to get wrong in the first place.
Please always use parens for that kind of stuff, because even if YOU memorized and love to depend on and show off your knowledge of the precedence and associativity rules of the language you're using, the people reading and modifying and maintaining your code probably don't.
https://wiki.php.net/rfc/ternary_associativity
>PHP RFC: Deprecate left-associative ternary operator
>Unlike most (all?) other languages, the ternary operator in PHP is left-associative rather than right-associative. The left-associative behavior is generally not useful and confusing for programmers who switch between different languages. This RFC proposes to deprecate and remove left-associativity for the ternary operator and require explicit use of parentheses instead.
>As an example, the code
return $a == 1 ? 'one'
: $a == 2 ? 'two'
: $a == 3 ? 'three'
: $a == 4 ? 'four'
: 'other';
>would in most (all?) other languages be interpreted as return $a == 1 ? 'one'
: ($a == 2 ? 'two'
: ($a == 3 ? 'three'
: ($a == 4 ? 'four'
: 'other')));
>which is both the useful and intuitive interpretation. In PHP, it is instead interpreted as return ((($a == 1 ? 'one'
: $a == 2) ? 'two'
: $a == 3) ? 'three'
: $a == 4) ? 'four'
: 'other';
>which is generally not what was intended.https://stackoverflow.com/questions/20559150/ternary-operato...
>Q: Can someone please explain what is happening here and why it is printing 'four'?
>A: Because your whole expression evaluates as if it was (......) ? 'four' : 'other'. Since the first element is probably something truthy, it gives you 'four'. In saner languages, where ?: has right associativity, the whole expression evaluates as if it was $a == 1 ? 'one' : (......), where if $a is not 1, you go on to test other things.
>PHP Sadness: Ternary operator associativity
>The ternary operator is left-associative and therefore behaves entirely incorrectly:
https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/
>PHP: a fractal of bad design
>Unlike (literally!) every other language with a similar operator, ?: is left associative.
https://en.wikipedia.org/wiki/APL_(programming_language)#Des...
>All primitives are defined to have the same precedence, and always associate to the right. Thus, APL is read or best understood from right-to-left.
But consistency across all languages is too much to ask for, except when one language is purposefully imitating another language's syntax but subtly changing it, like PHP did to C. Or the way Perl and PHP imitated C's syntax for references with &, implying that they were somehow alike and could be used in similar ways, even though they're totally different foot guns.
At least APL and FORTH and LISP pick a lane (albeit different sides of the road), and stay in it.
x = ( 1/y if y > 0 else
-1/y if y < 0 else
nan())
idiom.For readability.
I don't like it much, but it's neat that the "happy" path value is a prefix of the assignment statement, i.e. in
x = 4 if thing() else -1
there is a prefix x = 4
which, if the predicate is "expected," is like saying x = value (... unless blah blah)
which, I could see some people liking (maybe Dutch people).Let's just write everything in Scheme for God's sake JOIN US.
> x = value (... unless blah blah)
Except it's the opposite of that, because "if" is the opposite of "unless". It's more like saying
x = value (... if blah blah) x = value (... unless this isn't true, then it's blah instead)
The stuff in parentheses is a caveat. Works just as well your way, and more directly. x = value (... so long as this is true, otherwise blah)I’ve never figured out those question marks. And in your example, what the hell even happens if condition is true or false? At least the lisp version has a function call that includes the word set, so you can kinda guess that the x variable is set.
The if-expression reads very close to English, so you don’t need to look it up in the manual to read it.
I wouldn’t even know what to query to look up `code ? Code : code` in a manual / google. `setq` is also easy to look up.
The question mark colon loses on all counts in my book. I hate it.
It should be:
x = condition() ? 4 : 5Use umbrella if raining, else wear shades.
Personally I prefer FORTH, which like Yoda reads, and unambiguously puts the condition first, the IF second, the ELSE third, and the THEN last.
FORTH ?KNOW IF
HONK!
ELSE
FORTH LEARN!
THEN
FORTH doesn't have expressions, parens, precedence, or associativity rules, it just has a stack (or rather, two stacks: operand and return; or three of you count the vocabulary search stack). ( And actually it does have parens, but they are for comments! ) thing_to_take = umbrella if raining else shades
while in OCaml/English it would be: thing_to_take = if raining then umbrella else shadesPersonally, from a syntax point of view, as opposed to an easy availability point of view, I prefer Haskell, or Lisp if you can live with the parentheses. Haskell has some syntactic oddities, and the support for infix operators has encouraged the ecosystem to define way too many obscure </?$+/>-looking things, but the syntax mostly just gets out of your way and is relatively regular.
I do admit that if you're going to be infixy anyway, a C-like "?:" ternary is nice. C got its expressions right.
It is, of course, objectively true that one of the things Python does get right about syntax is the significant whitespace, and Haskell has that too (without the stupid colons). Curly braces are the work of the Devil, and "BEGIN" and "END" are just unmentionable.
Haskell got it right.
For data science stuff: Julia
x = 5
x = 4 if condition() def foo(things: Optional[List[str]] = None) -> None:
thing_list = things if things else []
... x = condition() and 4 or 5 x = condition() and [] or True
is not equivalent to x = [] if condition() else True
Now, I have seen a genuine application of `X and A or B` before. That occurs when you're checking a condition before, say, popping from a list -- but if the list is empty, you still want a default value.Try it out. https://ideone.com/GciAs8
True and False or 5
returns 5 rather than False. a = [1,2,3]
# list comprehension with if
[ x for x in a if x > 1]
[2, 3]
# list comprehension with if/else
[ x if x > 1 else x*2 for x in a]
[2, 2, 3]
When it's just "if" it goes after the "for", when it's "if/else" it goes, all of it, before. I still don't understand why it's this way, it doesn't even make sense even reading it in natural language. I much prefer the mathematical-like syntax present in Scala for this.Edit: Thanks for the responses, they are are insightful, never thought of this that way.
Still in my head the lack of "else" clause implies filtering no matter where, while its presence implies transformation no matter where. The first element of the list is always transformed (though in this example it's identity).
I think it's a case of Python trying to do too much with too little. Personally for anything relatively complicated I still prefer to use filter() and map() as list comprehensions can get unwieldly quick.
In your case you were conditionally choosing a manipulated return value for x and didn't care at all about filtering the list. Here's one with both:
[x if x > y else x*2 for x in a if x % 2 == 0]
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
return x as... filter down to only...
x or x*2 even 'x's
There's not really a semantically logical place for an "else" in the latter half because it's only looking for a true statement so it can return x (or not).For example this makes sense:
... if x % 2 == 0 or x == 3
(filter the list to even values of x or the value 3)but this does not:
... if x % 2 == 0 else x == 3
(filter the list to even values, otherwise wait, otherwise?)The problem is that functional-style fails to separate essential bits and fails to give strong visual clues where each bits begins and ends.
That is why the old imperative style is clearer. Compare:
for i in container:
do something with i
and [do something with i for i in container]
One has a clear separation of loop and action, the other requires scanning for the "for i" in the middle. This generalize to other similar construct, and the Python ternary operator has the same flaw. The imperative if / else version clearly separate alternatives on separate lines.IMO, syntax design should always try to learn from the imperative style and always strive to produce a syntax that clearly separate each logical item in a compound statement, and using different lines is one of the best for us human to visually grok.
(That's why even Lisper will often format their code to separate bits on different lines!)
OP clearly stated he didn't understand it:
> When it's just "if" it goes after the "for", when it's "if/else" it goes, all of it, before. I still don't understand why it's this way
and discussing it here on HN shouldn't be punished.
...
Back to the topic at hand ...
List comprehensions save you enough syntax to be worth it, IMHO:
mylist=[]
for i in container:
if i > 10:
mylist.append(i*2)
vs mylist = [x*2 for x in container if x > 10]
... That's a very terse, clean request for work and arguably it's easier to read than "for(int i=0; i<10; i++)", which we've all acclimated to by the end of CS101. I think they're quite worth the trade-off. >>> list([ x if x > 1 else x*2 for x in a if x <3] )
[2, 4]I feel you. I still use them though because they are still more readable then alternatives.
Ternary expressions on one line require branch coverage to be enabled in your e.g. pytest; otherwise it'll look like the whole line is covered by tests when each branch on said line hasn't actually been tested.
.get() -> Union[None, T]Sure that nice Beeping Busy Beaver uses only one loop, very cool. The "guessing game" program that almost everyone wrote when learning programming also uses one loop.
I'll now go work on my linter for blog posts that forbids using bold text. The Bible didn't use any bold text, so why would a simple blog post need it?
Just because it’s used by very beginners it doesn’t mean it’s good. That’s, like, the opposite of the truth. Beginners also clutter their code with endless “else if” sequences.
You just complained about strawmans and followed with a strawman.
I don't remember saying anywhere that this guessing game was good. You're putting words into my mouth (keyboard?) here. What I meant to say was that saying that the Beeping Busy Beaver only needs one loop doesn't add anything to the discussion, since a simple guessing game also needs one loop. Quoting the article:
> If you have a while loop and you are not working on something that requires unbounded computation, chances are that it can be rewritten more clearly as a for loop. I ran this check against the Pylint codebase itself, and I was shocked by the hideousness of some of the while’s that turned up. They couldn’t all be rewritten, but many could.
> Even the recently-discovered Beeping Busy Beaver champion program only uses one unbounded while loop, and that’s as a kind of toplevel program driver.
Here's how I would put it, in a kinder and more humble way:
"Most of the code I write only needs one main loop, and usually I'm not the one writing it. When I'm using something like Django, I have no need for a while loop, as it's already handled by the framework. In that case, I want the linter to highlight these loops. They are a potential danger, and something I want to pay special attention to. Even when I have to write myself that loop, I want to clearly separate the main loop from what runs into it. In my experience, this leads to code that is more readable, more maintanable, and that has less bugs. Thus, this is a good linter rule."
> Beginners also clutter their code with endless “else if” sequences.
Well, it's not like they can use a switch, considering it's Python. Or are you referring to pattern matching? Or you think using a dictionnary would be more idiomatic? I'm not sure what you're trying to say here.
The part about the Bible and bold text was a parody of the article to quickly highlight what's wrong with it. I personally dislike bold text. I think people shouldn't use it most of the time. Thus I did the same thing as in the article: I used an appeal to authority (the Bible, equivalent to the Beeping Busy Beaver) to say that everything smaller than the Bible shouldn't even use bold text, as the Bible doesn't need any.
Hah. Once-upon a time, I relied on bold text a lot for emphasis. Then I learned that its a crutch for bad writing, so I made an effort to use it very sparingly. I think my written material has improved substantially because of it. I think keeping bold text to a minimum is a good rule to follow when writing.
The nice thing about ternaries is that they're expressions, so they don't "infect" our code like statements (e.g. 'if'). For example:
if xCond:
x = x1
else:
x = x2
if yCond:
y = y1
else:
y = y2
if zCond:
z = z1
else:
z = z2
foo(x, y, z)
This lint rule will tell us to do the following instead: x = x1 if xCond else x2
y = y1 if yCond else y2
z = z1 if zCond else z2
foo(x, y, z)
Now that our branching is done with expressions rather than statements, we can actually in-line them, which seems much nicer (less chance to get them mixed up; no need to invent new names; no side-effects, e.g. accidentally overwriting an existing variable; etc.) foo(
x1 if xCond else x2,
y1 if yCond else y2,
z1 if zCond else z2,
)
If we really want the names, we could define them using the 'walrus operator': foo(
x := (x1 if xCond else x2),
y := (y1 if yCond else y2),
z := (z1 if zCond else z2),
)
Personally I would use separate 'x = ...' statements rather than :=, since we want to explicitly perform effects (binding names). f(x=a if b else c)
Should be valid. All the walrus gets you is making the x "infect" the surrounding namespace which is surprising.Yes, that's what I intended. I was pointing out that the foo(x1 if ...) version does not behave the same as the separate 'x = ...' statements, since it doesn't bind the x/y/z variables.
This is usually an advantage (if we don't need those values for anything else), but for completeness I noted that we could use the walrus to recreate the exact behaviour (keyword arguments can't do it, since they only bind variables inside the function call, which (usually) has no observable difference to using positional arguments). You're right that the walrus's effect on the surrounding namespace is 'surprising', and that's why I would prefer to use separate statements if we really want those names defined (since that's less surprising).
Sure in small cases this small ternary use is great! However too many times I've seen people chain them for far too many characters just to be "on one line".
We don't use one letter variable names anymore, so in the same reasoning why should we do the same to our code?
I strongly disagree with the author's idea that shorter is better. Clarity trumps conciseness. I'll admit that not enough conciseness can impact clarity, but there's ways and means to clean that up that don't involve clever code.
To test whether those statements are guiding principles, or merely ad-hoc justifications, I propose the following pylint rules based on those principles:
no-ternary-sugar:
Replace 'x if y else z' with '{True: lambda: x, False: lambda: z}[y]()'.
This is less concise, but improves clarity by using booleans explicitly,
and making the delayed evaluation of the results clear; both of which are
implicit in the 'if/else' syntactic sugar.
no-elif-sugar:
Replace:
if foo:
bar
elif baz:
quux
...
else:
foobar
With:
if foo:
bar
else:
if baz:
quux
else:
...
else:
foobar
This is less concise, but makes the branching structure clear, unlike the misleading
"flat" appearance of the 'elif' syntactic sugar.
no-special-case-patterns:
Replace:
if foo:
bar
else:
baz
With:
match foo:
case True:
bar
case False:
baz
'if/else' is syntactic sugar for pattern-matching a boolean, which is left implicit.
Explicit matching improves consistency with other use-cases, and makes the relationship
between branches and boolean clear, at the expense of conciseness.
(Also applies to single-armed 'if', which will only have a 'case True:').
Of course, the combination of no-elif-sugar and no-special-case-patterns would give extreme clarity like this: match foo:
case True:
bar
case False:
match baz:
case True:
quux
case False:
...
case False:
foobarEveryone has their own 'style' they like. It is usually not that big of a deal. It becomes a big deal if you get someone on the team who becomes obsessed with it, or someone who is always sloppy. Compact styles tend to be harder to decipher than simple ones. As when you are reading them they usually are not the same style context as all the other code around it. So it causes your brain to have to stop and figure it out. With python sometimes those compact styles actually run faster, so they can be handy to know about (test it though).
In java there is this thing where many will do things like blah = x().y().z(); Yet at any point in that chain something could crash out and return a null. It is compact for sure. But really is a pain to debug, but easy to read. Yet a lot of what is going on is burred in the 'middle' what if something in the middle is returning the wrong thing and the next thing in the chain happens to have the right method?
Compact styles can be easy to read sometimes if you know what that style is. You can also very easily introduce very subtle bugs. You can also convey the wrong meaning to the next poor soul that has to look at your code 3 years from now, 2 years after you left the company.
I use this style as sparingly as possible and fall towards verbose and spaced out code. I try to make it easy to read and broken down as best as possible. 6 months from now my tired brain will thank me.
For something like this example if I ended up with an if tree like that I would look at the underlying data structures. There is a data problem here and there probably would be a better way to do it.
I see this so often that I wrote a blog post about it http://chriswarbo.net/blog/2020-02-08-clever_code.html
tl;dr trying to make things smaller isn't "clever", it's code golf; often, "clever" solutions just-so-happen to end up small. Likewise, spreading logic over many lines can lose abstraction; we can end up lost in a tangle of bools, ints, etc. without seeing the bigger picture.
Getting that 'terse'/'readable' balance right can be tricky. Usually if some code is 'hard to read' it usually means it needs a bit of refactoring to shorten/length it up and make clear (with comments) what each bit is doing. You go drop something like duffs device into the middle of a parser you should put a comment on that. As not everyone has heard of it. If the code is going to be used a couple of times a year and if it takes an extra 15 seconds, so what. Comment it with 'hey this would be a good spot for duffs device?'. Most of the type of code I write these days runs so rarely and can take a bit of extra time. I am also working with jr devs who may or may not have read up on every cool trick. I am also playing with some code I got from the net. Some of these things have 5 page long functions, yep... totally lost in abstraction.
While is strictly more powerful than for, for is strictly more powerful than foreach, foreach is strictly more powerful than map.
And yet 95% of the time, the power in map is sufficient. Therefore 95% of the time you should use map. When you encounter a foreach, you should be expecting non-purity. When you encounter a while, you know that it's doing some recursive operation that requires that power. If you have junior members of the team writing while loops where maps would do the senior members of the team who understand the nuance will take 10x more time to understand that code.
The same applies to statements/code blocks vs expressions. If all you are doing is assigning one value and have no other side effects, and you can do so in a way that's not overly nested, you should use an expression. If you can't, we have the more powerful statement/block structure to fall back on.
[0]: https://blog.codinghorror.com/the-principle-of-least-power/
It was interesting to find out how HTML was designed from the start to be simple and not a programming language on purpose for this reason!
regarding the `consider-ternary-expression`
if condition():
x = 4
else:
x = 5
allow me to put fck'n breakpoints - in this case the expression is dumb, but what if is some complex stuff? x = 4 if condition() else 5
is cute, but - remember, kids - not everything* should be an expressionregarding the `while-used`, while I personally prefer the legibility of the `for` loop, I find the following paragraph... at best wrong:
>>> A while loop introduces unbounded computation. Do you really need unbounded computation for your boring web app? I doubt it. In almost all cases, the loop can be bounded in advance, as in “do this N times” or “do this for every item in this list”. Those kinds of loops are guaranteed to terminate, and that’s a nice guarantee to have.
in python `for` loop are equally unbounded as `while` ones. Yes, most of the time you are looping over a (finite) list or a (finite) dict, but the concept underlying the `for` loop is the iterator, not the list. And iterators can easily be infinite.
Not only iterators, but also plain lists. Example:
a=[1]
for i in a:
a.append("the ride never ends")
print(i)Not when it's at the cost of readability. The example "better" code fails my readability test horribly. I'd gladly take C's ternary operator over this monstrosity:
> x = 4 if condition() else 5
So you can read both as a sentence. The arguable part is more about "condition and then thing" versus "thing if condition".
x = 5
if condition():
x = 4 (x = 5, condition())
is inconsistent state (using the word "consistent" in the same sense as the C in ACID).It really is clearest and safest to avoid inconsistent state, even if is only transient. There are so many ways programs can be wrong; no need to deliberately create inconsistent state when there's no need to.
Also, your version isn't safe to use with objects (e.g. 'myObject.x = ...'), since the initial assignment could trigger arbitrary code (properties, __setattr__, etc.).
Also, your version isn't safe to use when the right-hand-side has effects, e.g.
x = fetch_config_url() if remote else read_config_file() x = if condition() then 5 else 4
I dislike how the regular order of if is changed when used as an expression. I don't know how you could retrofit that into existing Python tough. Probably a consequence of defining blocks with whitespaces.> Special cases aren't special enough to break the rules.
> There should be one-- and preferably only one --obvious way to do it.
Both are broken by the existance of 2 if syntaxes depending on the context.
Infix "if" conditions just scramble up the order of evaluation from the order of program source code.
If the condition-guarded statement is nontrivial and you remove the assignment, everything stays valid. If otoh you use a ternary or
if x:
y = 1
else:
y = 2
If you now delete the else block or something (like the assignment from a larger else block), a linter can warn you that y is undefined. Otherwise you'd need a test to ensure correctness. [ x.attr for x in list if x in someset]
There are languages where this kind of thing would be considered ugly and you’re supposed to use map/filter but in Python they’re the best practice. Every Python programmer is already trained to read expressions like this.$user_authenticated = hmac_auth($_get['token']) === true || ldap_check($username, $config['ldap_restrictions']) || verify_credentials($_post['user'], $_post['pass']) === true;
Sorry for the php pseudo code, I'm on mobile. And this is a very friendly example of what I'm trying to get across. It's self documenting code and worth the extra bytes.
I've seen way too many insane conditionals to agree that less code is better.
For the cost of 4 extra characters, this line tells you exactly what it does even if you've never seen Python in your life.
I wouldn't mind it so much if I could write it as:
x = 4 unless shenanigans(); then x = 5
Yes, that is a semicolon. Fight me.Makes way more sense to my brain to read:
print unless $x ~ /end$/;
then
print if $x !~ /end$/;
I used to get flack for using not just if expressions as ternaries, but also for using unless. Then I started teaching PERL at my company and drilled it into all the fresh new minds.
my $x = do {
if (foo()) {
4
} else {
5
}
};
(and yes, I know, that example would look fine as a ternary - but this is meant to illustrate the syntax possibility, not where I'd specifically use it - and once the logic within one of the two conditional branches gets more complicated, switching to do+if+else can make for clearer code)Also doing that with if/elsif/elsif/else is often far more readable than nested/chained ternaries.
cssClass = 'selected' if isCurrentTab else 'deselected'Or in pseudocode
if isCurrentTab
cssClass = 'selected'
else
cssClass = 'deselected'
Even ternaries read weird. "css class is current tab HUH?? selected COLON! deselected"if condition(): x=4 else: x=5
I get that it's a "flex" or some sort, but honestly in production code my experience is that it reduces productivity. Programmers need a little more squinting to truly understand what that piece of code is doing.
I find it similar to run-on sentences in books - we don't like that, and in Business Writing courses they explicitly say to not do that. Code should be similarly readable.
So...you should do more per line, since code is all one-liners, its just a choice of how many, so if you don't like them, you should reduce the number?
Though Ruby’s unless modifier is often slightly better for readability.
int result = condition
? value * 12
: something_else();
and in the case where the condition is sufficiently complex: int result =
(
some_condition()
&& another_condition()
&& yet_another_condition()
)
? value * 12
: something_else();
For me, at least, this is entirely readable. The unfortunate bit is that there is no formatter in existence (yet) that can handle this for C, or really any other language with similar syntax.Python's "Black" formatter actually does the best job here, yet the python ternary syntax is still very verbose and strange IMO.
Prettier does it fine for JS, which uses C-style ternary syntax.
> Python's "Black" formatter actually does the best job here, yet the python ternary syntax is still very verbose and strange IMO.
To me, its quite natural when used sensibly, since if you drop everything after the if it is the normal-case value. Though I would slightly prefer if the ternary form was:
<default-valur> unless <alternative-condition> then <alternative-value>
instead of: <default-value> if <default-condition> else <alternative-value>> Though I would slightly prefer if the ternary form was:
Agreed, I do like that much better too.
Well, tough shit, because I closed the tab.
Hard disagree. Obviously, if the loop is a fixed number of iterations or iteration over a collection (which is trivial to identify by human inspection but probably not for a linter), a for loop is better but it is not uncommon to be looping until a (possibly compound) condition becomes true that cannot be expressed as a fixed number of iterations or (at least naturally; you can embed arbitrary complexity into a generator, though usually, absent unbounded recursion, that will just push the “while” down behind a layer of abstraction) as exhaustion of an iterable.
while True:
...
if condition:
break
...
and kinda wish there was a nicer syntax for this. let foo = loop {
// ...
if condition {
break value;
}
// ...
}If it existed, I’d be tempted to suggest a linter rule that prohibits break inside an until (it should only be used when the main exit condition is the only exit condition.)
loop
while!(condition);
...
end;
for while, versus loop
...
while!(condition);
end;
for do-while, and loop
...
while!(condition);
...
end;
in place of: while True:
...
if not condition:
break
...
Alas, it never really took off.Almost every other case, in my own code at least, is a bounded loop. Usually processing a collection, but also often processing bounded ranges.
It's pretty easy to write a generator that produces an infinite sequence. That generator, of course, would need a `while` loop, but that loop can be in an external dependency which the linter is not checking.
You also can read way more than you expected if you use a `for` loop to read from a socket, or a file, to say nothing about reading from /dev/random.
Not that it's a bad advice, but it's not a rule, it's more like a... guideline. Discretion is still needed.
>>> x = [0]
>>> for x in xs:
... print(x)
... xs.append(x+1)Since my gut feeling is that 99% of the code is always shit that should not have been written at all, I have coded a linter that simply flags every line of your code and strongly advises you to delete it. And also change your trade if you can.
This linter was a very good invention. But then I got caught up in the spirit of being fair and “eating my own dog food”, and ran the ultimate linter on its own code.
It told me the code was pathetic shit and that I should delete it, and never write a line of code again because very likely it was going to be same shit, but worse.
I followed its advice.
Then so does a for loop. There’s no difference in this regard between for and while loops: you can use both of them to express both bounded and unbounded computations, and in each case you need to read and understand at least what comes before the colon to know which it is.
# Unbounded
while True:
pass
# Bounded
while False:
pass
# Unbounded
for i in itertools.count():
pass
# Bounded
for x in []:
pass
And you can’t just say “but with for loops you only need to worry about the iterator, whereas with while loops you’ll probably need to worry about statements inside the loop in addition to the loop condition”, as seen in this entirely realistic function that is unbounded if source == target (assuming a port of the DOM API, so child_nodes is a live collection): def clone_children_to(source: Element, target: Element):
for node in source.child_nodes:
target.append_child(node.clone())
(I agree that for loops are generally preferable to while loops where feasible, but I object to the way you’ve expressed that paragraph, and the expressed reasoning underpinning the entire section is flawed, as priansh also points out.)Guido doesn't like it, though, but the community seems to.
In fact, the community is embracing "black" these days, the python equivalent of gofmt, exactly because we have better things to do than arguing over PEP 8.
Based on my 25+ years of experience I strongly believe that the "if/else" that this author complains about is actually BETTER than the ternary he recommends. This is yet another example of the endemic problem where software engineers think "harder to read is a virtue".
I disapprove of any linter that flags this idiom.
In addition to the GP's self deprecating sibling comment, I started using Python around 2.2, before there were even "True" and "False". So, seeing a 'while 1' loop is perfectly natural to me. But, I'm also perfectly comfortable with Python's "truthiness" in more places than most people are.
As a bonus: consider "while 'false' :" as a perfectly valid start to an unbounded loop.
That sort of thing is ubiquitous in dynamically-typed languages. Even HTML: the state of boolean attributes is determined by the attribute being set, regardless of its value, so <input disabled=false> will give you a nice disabled text box. Mind you, some attributes that you might think would be boolean actually aren’t, for varying reasons good and bad, e.g. autocomplete=on|off, aria-hidden=true|false.
http://python-history.blogspot.com/2013/11/the-history-of-bo...
Python doesn't have an “[repeat...]until” loop, which C misspells as “do...while”, which fails to express what is going on.
> so when you want to do a thing at least once, the simplest replacement is starting the loop with "while 1:" and ending the loop with "if ... break".
“while True:” is more idiomatic Python (“while 1:” works since 1 is truthy, but using a literal 1 for “True” is a C-ism, Python has True as a literal for quite some time and idiomatic Python uses it.)
x = {True: 5, False: 4}[condition()]
, or if you don't want to evaluate the values: x = {True: lambda: 5, False: lambda: 4}[condition()]() {"a": 1, "b": 2}.get(selector(), default_value)
for the else-case. x = (4, 5)[condition()]
for a bonus "what am I looking at again?"The reason is simple: visual code structure.
A bespoke if block provides you with appropriate visual semantics that that part of the code involves a decision point.
By contrast, the ternary operator is useful when you want to visually obfuscate that fact, and make the code terser and more focused on the fact that an assignment is taking place, whose value is determined by an expression (which upon closer inspection happens to be conditional).
Yes I know that in the age of super-fat IDEs everything is but a search away, but, I've just come off from reading that other article from today about the guy who handwrites all his code.
So clearly low-level visual structure and ease of mental debugging still matters, no matter how bloated and feature-full your IDE is.
But what do I know. I still code on vim/nano.
Sometimes more code is easier to read than shorter code. Even
This is one of the first things that I add to a new project, before I write one line of code: https://github.com/realm/SwiftLint
I write about it here: https://littlegreenviper.com/miscellany/swiftwater/swiftlint...
If the linter says your function is too long then you refactor it, you don't turn off the linter. You also fix it immediately, not "later" (broken windows theory) before the next offender copies and pastes the offending code or continues making it worse.
In underdeveloped countries, governments manipulate the definition of unemploment to make themselves look better. Does that solve anything? no. It actually makes things worse because now you cannot mobilize people and resources to solve a problem you don't have. It's wrong. And so is turning off linting.
Right, one of the many reasons why nobody (with half a brain) hosts a web server in Excel.
I wonder how much "boring web app" experience the author had. If I'm debugging on 11 pm why the webserver is timing out talking to microservice A, but only if it first opened connection to service B, and someone strolls along saying "Hey, your code is bad because it's using unbounded computation," then god help us, because I might lose it. (Well, shrug, the worst that can happen is that I may rage-quit on the spot. I'm not a very imposing guy.)
If you're in charge, that is...
* If you prefer, change 11 pm to 11 am and my point still stands (though I'll be less cranky and less likely to rage-quit).
They love linters, you sound like you hate linters. Don’t use them and let your colleagues deal with your unbounded computation.
scale = 5
while (scale > 0.1):
do_stuff(scale)
scale = scale/2
Sure, you could use exponentials with range(), but is that really clearer? scale = 5
# The alternative would be exponentials with range(),
# but it's clearer to use 'while'
# pylint: disable=[while-used]
while (scale > 0.1):
do_stuff(scale)
scale = scale/2
Less sarcastically, how about: scales = itertools.takewhile(
lambda n: n > 0.1,
functools.reduce(lambda x, y: x / y, itertools.repeat(2), 5)
)
map(do_stuff, scales)
This would be even easier with an 'iterate(x, f)' function which generated (x, f(x), f(f(x)), ...), but I couldn't find one in Python's builtins: scales = takewhile(
lambda n: n > 0.1,
iterate(5, lambda x: x / 2)
)
map(do_stuff, scales) iterate (/2) 5.0
& takeWhile (>0.1)
& map do_stuff
The `iterate` generates the infinite sequence, the `takeWhile` specifies how much of it to use, and then we do stuff with each element.This is arguably a little nicer than the while loop since it exposes the real sequence of values you’re working with more explicitly, but at the very least it is comparably simple in its appearance. The Python version in the GP comment looks bad mostly because of the clumsy lambda syntax.
Also, I would scream if some opinionated dev gone crazy with their linter added pylint disables and comments explaining it every time we use a while loop in our codebase.
Why does the linter rule not, instead, check if the while loop is unbounded and warn the user of that? Surely screaming fire when there's an actual fire is better than screaming fire at the first sign of smoke.
Indeed, the linter only needs to incorporate of the many known solutions to the halting problem.
That sounds like a bad situation, but it completely depends on "every time we use a while loop". The entire point of lint checks is to reduce the occurrences of certain patterns (like while loops, in this case). This post is arguing that "every time we use a while loop" should ideally be the same as "never", in which case you're still correct, but vacuously so.
For a less controversial example, try running your comment through sed 's/while loop/eval/g'. There are certainly situations where 'eval' is the only way to do something; and other situations where 'eval' would be more readable/efficient (e.g. compared to writing a file and spawning a subprocess). Yet we go out of our way to minimise our reliance on 'eval', and I certainly wouldn't mind adding a pylint-disable comment for those times I use it (every few years).
scale = 5
t = [1]
for i in t:
do_stuff(scale)
scale = scale/2
if scale > 0.1:
t.append(1) for (my $scale = 5; $scale > 0.1; $scale /= 2) {
do_stuff($scale);
}Could you write them as a for loop? Possibly, but why? Is the new 2021 programming fad hating on while loops? Who do I contact to exchange my "js bad" t-shirts for "while bad" laptop stickers?
The logic behind disapproving of while loops in the article seems to stem from the author assuming while loops are "unbounded." This is absolutely, completely, totally false and there are entire branches of reasoning dedicated towards observing the bounded-ness of while loops. Most colleges include this in their curriculums and you usually have to learn to prove (via induction) the variants and invariants surrounding your loop before you can pass basic classes. It's even more ridiculous that, in the same breath, the author assumes for loops are guaranteed to complete execution. I can disprove this in two lines of code and two braincells:
for x in infinite_generator():
do_something()
This is also the general structure that message-queue libraries like Kafka, RabbitMQ etc take with their python code. Not to mention that, in many programming languages, for-loops are actually implemented as while loops behind the scenes.I'll cede that it's easier to write unbounded code with while loops than for loops, but this is programmer error that can be pretty easily avoided by simply being a teensy tiny bit careful (and can be easily corrected). I'd also argue that the majority of these cases where while loops can be substituted by for loops, are also cases where writing a while loop is much simpler than trying to convert into a for loop. (Besides, you could make any of these arguments about recursion.)
> You know what else doesn’t have unbounded loops? Excel.
This is also a lie, Excel absolutely has infinite loops, it just yells at you when you do it. The equivalent of this in programming is a linter. Instead of just screaming fire every time the user writes a perfectly fine while loop, why not concentrate your efforts on identifying unbounded loops and introducing a linter rule for that?
Honestly, it's absolutely terrifying to me that someone contributing to the predominant linter for a major programming language not only wholeheartedly believes while loops are always unbounded and usually evil, but also managed to get this introduced (and presumably approved) by reviewers of this linter, and then proceeded to gloat about it on HackerNews. It comes as no shock to me that most people I've worked with disable pylint in their editors if they're this unreliable with their review process.
It depends on the type of programming you’re doing.
Algorithmics routinely does stuff that matches while loops and C-style for loops while loops, but doesn’t match iterator for loops very well.
In most parts of line-of-business sort of software development (and that’s the significant majority of software development, frankly, certainly a lot bigger than algorithmics), I’d say it’s rare for a while loop to be desirable: it should almost always be iterator-powered instead.
I think the C-style for loop thing is a particularly interesting aspect to this: I suspect most code that uses while loops where an iterator would not be appropriate would actually be at least as well-served by a C-style for loop. But Python made a deliberate decision not to have C-style for loops, because most uses of them are better-served by iteration. And so this nudging from while to for is really a perfectly natural extension of that.
Still, all things being considered, it’s not the sort of lint that I would ever turn on for myself or for code that I review, because I know when to use each.
I'd find that pretty annoying.
Consider a while loop in a thread:
while not quit_thread_requested:
# do threaded taskI also think there were good reasons the author's linters were left as optional, and its probably not because everyone thought they were great ideas.
This trope needs to die. While loops are a basic language feature. A thread pool is one very specific example of where that language feature is necessary. Every very specific example is trivially dismissed with "the vast majority of of people aren't doing that very specific thing." It's the laziest rebuttal, and not even wrong.
Cool things I've seen written in python that need while loops: http servers, games, more generally anything that listens on a port or depends on user input, mathematical code that depends on computing a base-k representation, stack-based algorithms...
Hold on, let's stop there. Python sucks at recursion. So bad it's capped at a very small depth. The only reasonable workaround is to avoid the call stack and roll your own, which involves... you guessed it, a while loop. So, this linter prefers buggy stack-smashy code. That's nice. Adults can use another linter.
When I started learning ruby a bunch of the changes flew over my head, but it is also an excellent entry point to look further into best practices, and concrete examples of how my code could be better.
The condition is of course the tool being itself extremely good, otherwise it's just everyday hell (looking at you, phpstan...)
If you think expecting people to achieve Python mastery is unreasonable, wait until I tell you about language called C++.
I would never demand this for someone writing a Python app or module that out team would use as a normal import, just the stuff we actively manage.
Probably the whole concept of idiomatic Python is that there shouldn't be different styles of Python to which you graduate according to skill or experience, but rather that the simple, readable way of writing things should be done by everyone, always, from novices to experts. Code-golfing with dense weirdness would be "anti-Pythonic" but this ternary expression would be readable even for if someone who's never seen Python would read it as pseudocode in some theoretical CS paper.
In Haskell there is a higher order function for basically every usecase and (most of the time) the compiler is smart enough to merge chained higher order functions into a single loop.
Python is at least trying with its `itertools` package, but it's still a far cry from the generalization "modern" languages allow.
(Background: I was fortunate enough to program in Haskell for 2 years. It was great, but I can see that it's basically useless for a lot of common usecases :-( )
while self.keep_going:
s = input('> ').lstrip()
if s == '':
continue
cmd = self.parse_cmd(s)
# The "quit" commands sets self.keep_going to False, oh the horrors
# of the global mutable state
cmd()
? Should it be: for cmd in self.stream_of_commands():
cmd()
But stream_of_commands() will still have a loop inside it, wouldn't it? Because that's how you write generators?(The author is on point when they say almost all code used in practice is primitive recursive, though)
And no, they aren't othogonal. A simple language with only integer variables, basic arithmetic, if() and for(i in range(x,y)) is actually not Turing complete.
(if a project unconditionally bans PRs that don't pass the linter, that's a different issue, and not really the linter's fault)
The trick would be making it sufficiently useful and minimally annoying that people don't get angry and turn it off.
when using the python integration in vscode (which runs pylint), clicking on a linter error gives me a context menu where one of the options is essentially "are you sure?" and will automatically adds the appropriate comment to disable the linter for that line.
The ternary operator is mostly a matter of style, and just be aware that for loops is often a better way to iterate through things than while loops. I wouldn't worry too much.
I can’t imagine making a game (though not sure who makes games in python) without a while loop to run on the main thread.
...And I wrote 'generally eager', because having a linter raise pedantic concerns over trivial matters is a way to extend this problem across from-scratch code as well.
while not quitRequested:
processNextEvent()
Should be rewritten to this? for i in range(999999):
if quitRequested:
break
processNextEvent()
Because that's supposedly guaranteed to halt? If so he's either joking or crazy. And his program will crash for his poor users after 999999 events.> anything that runs for as long as the user says it should run;
Per the article...
Probably not for me though.
while not loop.quit_requested:
process(loop.next_event())
And when you see it like that, then it becomes more apparent there’s an obvious way for it to be an iterator: for event in loop.run():
process(event)
This is typically harder to get wrong, and generally matches the semantics of what you’re trying to do more closely: process a stream of events. You didn’t actually care about quit_requested, you cared about the events.And so in this you can start to see why iterator-based for loops are normally better than while loops (and C-style for loops, which Python doesn’t support): while loops (and C-style for loops) are generic bookkeeping, with no specific semantics on the while condition (or for init/condition/increment clauses); but iterator-based for loops operate on actual data.
This principle can be seen in resource locking also; you don’t want a lock object beside the data that it locks, like this:
with lock.acquire():
queue.append(item)
That style is just asking for trouble; sooner or later you’ll touch the queue without acquiring the lock, and everything will fall apart. Instead, you want to lock the actual data so that you can’t even access the locked thing without acquiring the lock: with queue.lock() as q:
q.append(item)
P.S. If you deal with something like the original loop without a run() method, you can write a generator to convert a while loop into a for loop: def run(loop):
while not loop.quit_requested:
yield loop.next_event()
for event in run(loop):
process(event)
Most of the time the difference won’t be significant or worth it, but sometimes this can really help to clarify things, by extracting the loop bookkeeping into one place so that you can do what you were semantically trying to do, processing a stream of events.P.P.S. Instead of range(999999), use itertools.count() when you want unbounded iteration. Or itertools.repeat(None) if you don’t care about the number—but in that unused number or None see that a for loop is probably not the right tool for what you’ve written: you would be better either shifting back to while loops, or iterating over the data as I’ve demonstrated.
For other languages, you might still hit stack overflow (not the site) before you get to 10000 if the stuff you push on the stack is large-ish.
Do you really need unbounded computation for your boring web app?
Because everyone using python is working on web application. Also because all web applications have the same type of business logic.I do like linters, the best ones help avoiding mistakes and ensure some consistency in the codebase, but this article goes a bit too far.
print("yes") if random.choice([True, False]) else print("no")
Does this do the right thing? I was pleasantly surprised to find that this is indeed lazily evaluated, but that's not at all intuitive: first because `print("yes")` comes before the conditional (note that the Lisp example had the conditional first), and second because not all popular languages work like that.There are lots of examples where ternary makes things obviously harder to read, such as when at least two of the three expressions are non-trivial. Which one would you rather read?
if some_complex_condition(using, four, different, parameters):
do_a_thing(now, using, five, different, parameters)
else:
do_another_thing(with, three, parameters)
or do_a_thing(now, using, five, different, parameters) if some_complex_condition(using, four, different, parameters) else do_another_thing(having, three, parameters)
(or do_a_thing(now, using, five, different, parameters) if some_complex_condition(
using, four, different, parameters
) else do_another_thing(having, three, parameters)
after Black.) do_a_thing(now, using, five, different, parameters
) if some_complex_condition(using, four, different, parameters
) else do_another_thing(having, three, parameters)There is a very simple rule avoiding most of these issues - if the ternary expression does not fit in one line, use if/else.
If that's the case, maybe the problem is the if-expression, and not the programmers?
If you have a syntactical construct in your language that the programmers are avoiding in general, then that construct has problems.
Less time bickering means more time spent doing actual productive stuff.
(Both of these statements can be true.)
- extensive small functions/methods
- people who don’t use early escapes
Conciseness is not necessarily better.
- Does this idiom make debugging easier? no, it doesn't.
- Does this idiom make it easy to identify where a branch starts and ends? no, it doesn't. Before you could use indentation alone, now you have to read the statement.
- Is it slightly faster to type? Maybe. But that does not matter because time spent typing is a tiny tiny fraction of the time you spend as a developer. You spend much more time reading than typing.
Someone needs to inform this guy that generators exist
x = if condition(): 4 else: 5 def if_then_else(cond, true_case, false_case):
if cond:
return true_case
return false_case
x = if_then_else(condition, 4, 5) x = if_then_else(condition, calc1(), calc2())
Here it will call both calc1 and calc2 even though it will ignore one depending on condition. They might be expensive operations, or might have side effects that you don't want. The original code does not evaluate the part that is not used