Hacker News new | comments | show | ask | jobs | submit login
PEP 308 and why I still hate Python (dvt.name)
37 points by dvt on Mar 10, 2017 | hide | past | web | favorite | 74 comments

To be fair, Python isn’t terrible, but throughout its lifetime, it made some incredibly poor design decisions

Substitute {{x}} for Python and you have a template for a Hitchhiker's Guide entry for just about every programming language, ever.

I agree. The more mature developer understands all languages have their strengths and weaknesses. I really like Python myself, and it's in my top three list of languages I like (Scala, Ruby and Python -- not in any order).

Even PHP, which is not a well designed language at all, has at least attempted to make core language improvements in PHP7 and has supersets like Hiphop/Hack which use the same syntax and avoid most of the insane typing pitfalls.

And in this case, the reality is the ternary operator is just a pretty bad syntax. Most teachers generally tell you to stay away from them and a lot of style guides do too.

Python tried to make it more english-like just like many other of it's syntax (using and/or/not instead of && || !) and in this case it feels strange to experienced programmers who are not used to it. That being said, if you show this and javascript one to someone who's new to programming, they'll for sure understand the Python one better.

Still, again that's where style guides come in. I've been coding Python for years and rarely ever used the ternary.

Yup. And Python is certainly one of Stroustrup's two kinds of languages:

"There are only two kinds of languages: the ones people complain about and the ones nobody uses." - Bjarne Stroustrup

I can't think of a language that belongs less in that dichotomy than Python. Programmers used to gush over it like crazy; they only stopped because it became ubiquitous enough that there was no point. Javascript, PHP, and C get an order of magnitude more hate.

but lisp...

(occasional lisper)

Yes lisp.

(constant lisper)

The difference to me about this is that it seems PEP308 was explicitly created to spite those wanting a conditional operator, whereas many other "incredibly poor design decisions" like e.g. PHP's conditional operator having the wrong/unintuitive associativity happened more accidentally/without explicit intent.

I've yet to see one of those "let me rant for ten pages about what's wrong with {{x}}" articles for Elixir, but it's probably just a matter of time.

Python is an imperative language. The control structure, based on indentation, is well matched to Python's imperative character. Attempts to retrofit a functional style to Python fit Python's syntax badly. Lambdas, conditionals, and list comprehensions look painful if they extend beyond one line, because the indentation assumptions of Python just don't fit.

As a practical matter, only use Python's functional features in one-liners.

Python is an imperative language.

...as are many others with the conditional operator's operands in the normal (condition, true-case, false-case) order:


I think being "functional" or otherwise has nothing to do with it. Even in imperative languages you must use expressions, and it is there that the conditional operator is most useful.

Yeah, this has nothing to do with FP. No idea what the parent was going for...

Indeed. I find the huge "some_method" example pretty much invalid because I'd assign that dict to a variable first, so the line would be a nicer "return my_dict if y else {}". What person, style guide or PEP is forcing anyone to write some_method's return statement?

I don't get the ideal of saving space and writing huge one-liners (that in Python you'd have to break up over more lines). As they say on IRC: "if you're worried about using too many lines, I can send you a file full of blank ones."

You've identified a real issue (the functional features work badly except as one liners), and a good fix (only use them as one liners).

I think the cause has everything to do with some poor choices around the parser and syntax, however, and nothing to do with its imperative nature as such.

I'm not sure why the author calls the two branches of in if/else the consequence and the final_consequence. That naming suggests that you do both of them, with the latter coming after the first, which is of course not how an if/else works.

I'm sure that nearly everyone reading the article knows how an if/else works, so that naming won't actually lead them astray, but it just feels ugly.

Bad wording on my part, point taken.

So, Ruby has:

    consequent-expr if antecedent-expr
...which has all the same problems as the PEP308 example, as far as I can tell. Its (implicit) else-clause is just always nil.

I can't tell from the author's argument whether this condemns Ruby as well, or if something about the PEP308 style was uniquely bad.

I could see something definitely worse about the readability of this code:

    {long expr} if x else {another long expr}
but the author didn't discuss that case.

The difference between Ruby and Python here is that Ruby has a perfectly functional ternary operator that works as one would expect. It also allows some other crazy syntax which can be appropriate in some cases, but it doesn't force you to use it in all cases.

Python does force you to use their awkward conditional expressions; there is no normal ternary operator.

There's a big difference between a language which lets you write hideous code, and a language which forces you to write hideous code.

Python does not force you to.

In any case where you can use PEP 308, you can use a boring traditional "if else".

Is that intended as a defense of Python's ability to enable clear, concise, expressive code? :)

More seriously: Yes, you are obviously correct. But while you can use if/else, you can't use a ternary, and you can in Ruby. That's why people complain about the lack in Python but don't wrote the same article about Ruby.

"Clear, concise, expressive code" isn't something that applies to ternary operators. They were always meant as shortcuts and, as such, will never be as clear as if else statements, practically by design.

Though I don't really dislike Python's approach so what do I know.

You can definitely use too many ternary operators, but I think when used sparingly they can be clear and tasteful. As long as you don't start nesting them or repeating them one after the other you should be fine.

But you can't use a normal ternary operator...

The trailing if can sometimes be confusing in Ruby but the else clause is the thing that's just blatantly anti-intuitive about the Python syntax at issue. Ruby doesn't allow else there because that would be insane. Python chose the insane convoluted and confusing syntax over a well understood and accepted ternary operator.

Perl has the same type of syntax:

    print "true" if (a < b);
I recall seeing dialects of BASIC back in the 70s which supported it as well. Who knows, maybe this is where Perl got it from.

I use the post conditional most often in perl in this way:

    foreach my $foo (@collection) {
        next if (I-dont-care-about-this-$foo);
        next if (another-reason-for-ignoring-$foo);

I use ruby's trailing conditionals exactly this way all the time - it makes me happy to see someone else using the same pattern. I also use it at the start of functions in order to sanity check arguments:

    def do_something(arg1, arg2)
        return false if arg1.is_invalid
        return false if arg2 < sane
The trailing `unless` is also handy, and I find it a bit nicer to read compared to a negated `if`.

Reading it as "Alice studies all night if she goes to the library; if not, she'll sleep all night" seems perfectly natural.

Having said that, I often use the "else" as a "this should never happen" clause that raises an error.

I agree that it reads naturally, but when dealing with those kinds of sentences, they always need to be put into conditional form (A->B). Even when reading text the order of those words matters. Consider:

"I'm going to die ... (gasp) ... if you don't grab me some Subway. I'm starving"

The sentence is funny because the apodosis is misleading. Even thought that might be a valid narrative approach when writing a comedy, I don't think it's an appropriate coding one.

> The sentence is funny

Code should have literary value. ;-)

As a fellow member of the hates-Python-but-not-enough-to-quit-my-job-over-it club, comprehensions are just the worst. Uses variables before defining them, can't be readably broken over multiple lines, overloads multiple pieces of syntax, reads inside-to-out instead of left-to-right. Ugh.

What is awful about:

  foo = [transform(bar) for bar in baz.list() if condition(bar)] ?
Seems very readable and expressive. Or are you complaining about how they execute?

Also \ used judiciously with proper indentation can break statements over multiple lines without too much pain.

1: When I hit the opening bracket, I don't know whether I'm looking at an array literal or a comprehension. I have to read ahead a few tokens to disambiguate.

2: When I hit `transform(bar)`, I don't know what `bar` is either. I have to jump forward to disambiguate again.

3: Once I hit `for bar in` I realize I'm in a comprehension, but that's still not enough to figure out what `transform(bar)` is acting on. I have to read to the end of the `if` condition, then backtrack to the beginning.

bonus reason: why not `list(baz)`? Is there actually some logic to when Python uses methods and when it uses global functions? If so, please tell me.

another bonus reason: when I was learning Python, I found the keyword reuse genuinely confusing. I would try to write for loops like `for x in my_list if condition(x):` and couldn't figure out why it didn't work because I knew I had seen that syntax somewhere before.

Now, your particular example isn't that bad, but if you make it just a little more complicated you get something like this aberration:

    foo = [transform(bar) if bar.property else bar for bar in baz.list() if condition(bar)]
Nothing will convince me that you're not a mutant if you think that's clear and concise.

Meanwhile, over in Ruby-land, we write this:

    baz.each.select{ |bar|
    }.map{ |bar|
      bar.property ? transform(bar) : bar
Which to me parses very easily into meat-brain logic, in small chunks, with a single pass:

-Start with `baz`

-Consider each item

-Select only those `bar`s that meet `condition`

-Map each `bar` to...

-`transform(bar)` when `property` is true; itself otherwise (okay, natural language actually more closely resembles Python-style ternaries here. Sue me).

P.S. apologies for the multiple edits; I am an HN formatting noob.

> When I hit the opening bracket, I don't know whether I'm looking at an array literal or a comprehension. I have to read ahead a few tokens to disambiguate.

All research I have seen says that proficient readers of a language read closer to a line at a time than a token at a time, so I suspect that not really a problem so much as a rationalization of aesthetic preference.

> bonus reason: why not `list(baz)`? Is there actually some logic to when Python uses methods and when it uses global functions?

list is a built-in type, and (as is usually the case for Python types) list() is the type constructor. There are other global functions in Python and there is room for debate over whether they make sense over methods, but constructors for built-types are pretty clear.

baz.list() is a method on baz. Assuming baz is iterable, list(baz) is valid, it's just different than baz.list().

I generally like python's list comprehensions, but they have some ugly warts. For example, a simple comprehension looks like this:

  foo = [transform(bar) for bar in baz.list()]
Adding an if condition looks like this:

  foo = [transform(bar) for bar in baz.list() if condition(bar)]
But an if/else expression looks like this:

  foo = [if condition(bar) transform(bar) else other_transform(bar) for bar in baz.list()]
Adding an else requires completely rewriting the line! Super ugly and inconsistent.

Edit: formatting... don't know why my the 'list()' in my last example is disappearing

maybe it's bad form but I generally prefer to break my list/dictionary/set comprehensions into various lines. For me it increases the readability

    foo = [
        if condition(bar) transform(bar) else other_transform(bar) # 1: storage
        for bar in base.list() # 2: iteration
        if bar_tester(bar). # 3: filtering

to each his own I suppose, until the PEP8 police come and get us all!!!

Filters and ternaries aren't the same kind of thing (ternary if/else are analogous to SQL SELECT-clause case statements, filter if is analogous to SQL WHERE-clause; the positions in Python conditionals, which are very much like SQL SELECT statements, follows that analogy.)

Yea, I like list comprehensions. I mean they make sense once you're use to reading the syntax. That's just like the following in Scala:

   foo = baz.list().filter{ bar => condition(bar) }.map(transform(_)).toList
If it's your first time looking at Scala, you'd probably be like, "What the fuck is that shit?" but as you learn the language, these types of patterns become common and make a lot of sense.

I mean it's not like weird rules in natural language. At least the grammars for computer languages are strict, with very few edge cases (compared to human/spoken language).

I hope you mean:

    foo = baz.list().filter(condition).map(transform).toList
No need to create the extra anonymous functions.

Does Scala have line continuation? I got really used to pipes from R (which I guess come from F#/Elixir), which in turn got me really comfortable with multi-line statements like:

    foo = baz.\
        filter{ bar => condition(bar) }.\
IMO this is about as readable as it gets. It's like bullet points enumerating each step in the "algorithm" that produces a `foo`.

AFAIK Javascript also uses this pattern a lot.

I think that Scala uses semicolon terminated lines so you would write something like:

    foo = baz
        .filter{ bar => condition(bar) }

Scala doesn't require the semi-colon at the very end, but it does allow for them if you really want two statements on the same line.

Edit: and yes, that line separation works fine, both compiled and in the REPL.

BTW similar syntax in javascript, ruby, c#, etc.

Instead of asking "what is awful about X", how about considering "what would make it better"?

I think this would be easier to read, given that what's inside the [] now looks almost exactly like the equivalent code without using a list comprehension:

    foo = [for bar in baz.list() if condition(bar) transform(bar)]
(Disclaimer: I am not a regular Python user. Maybe that ordering conflicts with a syntax for something else.)

You don't need line continuations to split comprehensions because the delimiters already allow you to split lines.

I come from a mathematics background and when I first saw comprehensions I really liked them. They read remarkably similar to Set Builder Notation [0]. I think so long as you don't go crazy they are perfectly readable. But at the end of the day the onus is still on the developer to ensure their code is readable.

[0] https://en.wikipedia.org/wiki/Set-builder_notation

Bachelor's in math myself, although my math interests and my programming interests have admittedly (and regrettably) not intersected very much in my career. I do tend to think about problems more "imperatively" in my head and then struggle sometimes to translate those concepts into declarative math notation when doing proofs; if I were able to make that leap more naturally maybe I'd benefit from the notational similarities more.

Comprehensions were inherited from the math and science community where such constructs are used a lot. It happens that Python is very popular in those communities.

And does not allow access to the list being created from within the list comprehension. Which makes a whole slew of things hard to impossible using comprehensions.

Yup. Ruby blocks, FTW.

ruby blocks are exceptionally difficult to rationalize at first (well, at least they were for me). I get that they are beautiful (because they are) but it was a major stumbling block on my way to understanding ruby.

A cute feature of this syntax: in "p if c else q" you can think of "if c else" as an infix operator like + and the rest. This operator has an algebra worked out by John McCarthy -- e.g., it's associative. Of course I doubt this will help the poster to like Python more, but it might amuse others.

GvR seems to make readability a priority.

But my problem with

    true if cond else false
is it makes impossible to chain fast choices.

I thought it was a bug, then I understood it was a feature (after talking to him).

GvR hate ternary operators, the python 3nary is midfix, it sux, but it removes the possibility for people to do long chained if/then/else in unreadable ways.

If you think I am right and it sux, understand I think forbidding this kind of operator is like hating shortcuts and goto.

Since I love shortcuts, goto and chained ift and python, I logically found ways to not care of PEP308 because it is a non problem.

Dispatch tables, and/or used wisely ... there are a lot of ways to not care about PEP308.

> GvR seems to make readability a priority.

I don't find the syntax adopted in PEP 308 to be readable.

> GvR seems to make readability a priority.

And my point is that going against the standard (for over 100 years) way of reading conditionals is quite the opposite.

I do agree, and for me it is ok to disagree with GvR.

I would have prefered a ?: but I can replace it with

     # requires non null default else there is a bug 
     value = arg is MARKER and default or arg
So ... I don't bother

Why didn't Python do this:

  x = if condition a else b


I think it could be ambiguous in certain situations. Let's say I wanted to conditionally set a variable to one of two negative constants. In the current syntax, that would look like:

  x = -1 if y else -2
However, with your syntax, it would look like:

  x = if y -1 else -2
Which could be possibly confused with trying to subtract 1 from y.

Ah, and Python uses whitespace and colons to disambiguate, which would ruin the efficiency of this form.

Because what they actually want is something like:

    checkSomeValue() && doOtherThing()

Because that scans as:

Variable x should be assigned if condition a true, otherwise b.

But "condition" is just a placeholder. A concrete example would be:

   msg = if (status == 0) "done" else "error"

Take away the parens (which python is pretty explicit such structure should not be required to be legible).

msg = if status == 0 "done" else "error"

From a compiler perspective that's a bear to parse.

too much like perl?

I've been doing Python professionally for 13 years (among other languages, but Python is in the one I've used more). I like the language but I also hate some things (mainly, the GIL, the lack of static typing now lessened thanks to mypy and the slowness), but I never tough of the ternary if as evil or found a single instance where it confused me.

I rarely see PEP 308 in code bases

Which language do you like?

I like Java, Javascript, Go (which holds a special place in my heart), and C++ even. And I'm being a bit hyperbolic -- I don't hate Python, but sometimes I feel that the language works against you, and not with you.

You like JavaScript, the language where parenthesizing an expression can change the value of that expression[1]:

   > {} + []
  <- 0
   > ({} + [])
  <- "[object Object]"
or how the identity x|0 == x doesn't hold for all "integers"[2]:

   > 4294967296 | 0
  <- 0
or how function definitions aren't grammatically statements, making this[3]:

  function foo() {
    function bar() {} // legal
    if(1) {
       function baz() {} // illegal!
this just always bites me:

   > "a string" instanceof String
  <- false
(but seriously, I don't understand why JavaScript needed the object/primitive distinction. I get it for Java, but not in JS.)

> sometimes I feel that the language works against you, and not with you.

JavaScript is the language where you ask it, "here, I want to know which of these guns most adequately shoots my foot."

ES6 improves things considerably; in 10 years we'll have good browser support for basic data structures, like Map and Set.

[1]: I'm joking. Read it first, then return to this footnote. The sleight of hand is this: the first expression parses as an empty code block, followed by a unary plus taking an empty list. The second parses as binary addition. There's also a terrible amount of coercion taking place. This also "means" that binary addition "isn't" commutative: {}+[] and []+{} evaluate to different values; the same trickery is involved.

[2]: the binary or operator, |, silently converts the operands to a signed 32bit integer for the duration of the expression. Note that signed 32bit integer is a type not available to the JavaScript programmer — it only arises internally! (The only numerical type available is "Number", which is an IEEE double.)

[3]: the grammar for functions is essentially "list of statements or function definitions", so function definitions are only valid at the "top level" of a function.

True, JS is a pain in the butt. But I think it's fun: it feels fluid and that you can do almost "anything" with it (for better or worse). In Python, I feel that I'm fighting against arbitrary goalposts or that I'm wracking my brain parsing some logical puzzle (like in the PEP example).

Properly spec'd and implemented language structures are the opposite of logical puzzles. JS is not fluid, it's broken.

Nevermind that Javascript has a hideous side too: http://callbackhell.com/

And don't get me started with Java's verbosity. Half the time Java seems completely unusable without an auto-completing IDE.

And in all honesty I am a big Python fan, but only because it seems like the lesser of all evils in programming. There are some great designs out there, but Python is that toolkit that just seems to be pragmatic enough to be useful and idealistic enough (hello, significant whitespace) to encourage expressive algorithm design.

I started taking Scala jobs a while back and I don't think I'd every want to go back to a Java shop.

That being said, some things in Scala are difficult to wrap your head around, and anything made by TypeSafe/Lightbend should die in a fire, but overall I still enjoy coding in it.

Python has list comprehensions because maths has set comprehensions.

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact