PEP 308 and why I still hate Python

I’m not a Python guy, but it seems that every job I’ve had has slowly pushed me into doing more and more Python until I end up doing nothing but Python all day. And I hate doing Python all day.

To be fair, Python isn’t terrible, but throughout its lifetime, it made some incredibly poor design decisions. One such decision, and the subject of this blog post, is PEP 308 — Conditional Expressions.

A conditional primer

Before we dive into Python, let’s first look at the nature of the conditional; first, in grammar, and then, in logic. In English (a natural language), we often say things like:

$$\text{If Alice goes to the library, she studies all night.}$$

Linguists split up this sentence into the condition, known as the protasis — and the consequence, called the apodosis:

$$\overset{Protasis}{\overbrace{\text{If Bob goes to the party}}}\text{, }\underset{Apodosis}{\underbrace{\text{he will fail the test}}}\text{.}$$

Note that in ordinary speaking or writing, the consequence doesn’t always follow the condition. Consider:

$$\underset{Apodosis}{\underbrace{\text{Bob will pass the test}}}\text{ }\overset{Protasis}{\overbrace{\text{if he studies with Alice}}}\text{.}$$

But in logic, a symbolic language meant to represent (among another things) natural language, it does. In fact, in every major symbolic system (except for Frege’s idiosyncratic and two-dimensional Begriffsschrift, published 1879), the consequence, known as the consequent, comes after the condition, known as the antecedent:

$$\begin{array}{ccc}
\text{Russel & Whitehead (1910)} & \text{Łukasiewicz (1924)} & \text{Modern}\\
A\supset B & \text{C}AB & A\rightarrow B
\end{array}$$

The semantics are unchanged, so we can informally symbolize the above sentences like so (I do some pronoun substitution to make them less ambiguous):

$$\overset{Antecedent}{\overbrace{\text{Alice goes to the library}}}\rightarrow \underset{Consequent}{\underbrace{\text{Alice studies all night}}}$$

or, in the reversed case:

$$\overset{Antecedent}{\overbrace{\text{Bob studies with Alice}}}\rightarrow \underset{Consequent}{\underbrace{\text{Bob will pass the test}}}$$

This small example hopefully shows how a consistent formalism like a very bare-bones logic can make seemingly different linguistic constructs take the same shape. In other words, we now have the tools to turn all kinds of sentences into \(A\rightarrow B\) format. How cool is that!

Programming languages as formal systems

It shouldn’t come as much of a surprise that most programming languages take full advantage of these kinds of logical formalisms. In our case, we see if-then-else statements follow the same structure:

Logically, we can symbolize this as follows:

  1. \(condition()\rightarrow consequence()\)
  2. \(\neg condition()\rightarrow final\_consequence()\)

where \(\neg\) is the symbol for logical negation. We can also handle if-then-elif-else blocks like this one:

  1. \(P()\rightarrow Q()\)
  2. \((\neg P()\wedge R())\rightarrow S()\)
  3. \((\neg P()\wedge \neg R()\wedge T())\rightarrow U()\)
  4. \((\neg P()\wedge\neg R()\wedge\neg T())\rightarrow Z()\)

where \(\wedge\) is the symbol for logical conjunction (read as and). My examples all use C-style syntax, but many languages use this basic structure. For example Ruby:

Or Lisp:

And even the Erlang people do it:

Due to some implementation details, true -> acts as the “catch all” else but is generally avoided, per the (official?) Erlang tutorial:

else or true branches should be “avoided” altogether: ifs are usually easier to read when you cover all logical ends rather than relying on a “catch all” clause.

In fact, according to Richard O’Keefe:

It may be more FAMILIAR, but that doesn’t mean else is a good thing. I know that writing ; true -> is a very easy way to get else in Erlang, but we have a couple of decades of psychology-of-programming results to show that it’s a bad idea. I have started to replace:


	if X > Y -> a()		if X > Y  -> a()
	 ; true  -> b()		 ; X =< Y -> b()
	end		     	end
                          by
	if X > Y -> a()		if X > Y -> a()
	 ; X < Y -> b()		 ; X < Y -> b()
	 ; true  -> c()		 ; X == Y -> c()
	end			end

which I find mildly annoying when writing the code but enormously helpful when reading it.

Is else evil?

Although you might find some that disagree, else is not really all that bad. In fact, if we define “evil” as the degree of logical complexity, your else clause is only slightly more evil than your last elif clause — the else clause negates all branches, as opposed to elif which negates all branches, save one:

$$\begin{array}{cc}
\hfill\text{elif:} & (\neg A\wedge\neg B\wedge C)\rightarrow Z\\
\hfill\text{else:} & (\neg A\wedge\neg B\wedge{\color{red}\neg C)\rightarrow Z}
\end{array}$$

So if our argument is “else is evil,” it ought to extend to elif as well. I don’t think that’s a concession many would be okay with. Furthermore, else is sometimes downright necessary. Programmers are a lazy bunch, and at some point they all agreed that they would no longer want to write code like this:

Instead, it would be great to write something like:

And so, the conditional expression (also known as the ternary operator ? :) was born. Even though the syntax changed, the order of the operands (condition, A, B) and the semantics are unchanged:

$$\mathbb{\mathtt{x\:=\:condition()\:?\:A()\::\:B()}\equiv}\begin{cases}
condition()\rightarrow A()\\
\neg condition()\rightarrow B()
\end{cases}$$

But some things are evil. For example, Erlang’s ; true -> c() is mind-numbingly confusing — in the context of a conditional branch, the guard true makes no sense semantically. A potential solution is obviously to say ; X == Y -> c() instead, as O’Keefe mentions, but note that we lose some flexibility with this workaround (namely, we can’t express something like \((\neg A \wedge \neg B \wedge \neg C) \rightarrow Z\) in a straightforward fashion). We can, of course, cut Erlang some slack: it is, after all, a pattern-matching functional programming language, so not having a nice if-then construct comes with the territory.

Double-negatives are also evil. For example, code that looks like this:

should be refactored to code that looks like this:

The less “mental hoops” we jump through, the more readable our code is — or, the more sense our language makes. Next, I’ll show how PEP 308 is guilty of both semantic confusion (like we saw in the Erlang case), and making a reader jump through unnecessary hoops (akin to the double negation scenario).

PEP 308 is a bad idea

In Python 2.5 (around 2004), Python also decided they would implement a conditional expression of sorts. But, instead of using the usual ternary operator as C, C++, JavaScript, and many others use, it opted to go from a code block that would look like this:

to this:

The official docs call this syntax “surprising”. Not only is it surprising, it’s downright confusing. You might think it seems innocuously similar to the typical a ? b : c but the devil’s in the details. First of all, notice how it reverses the logical structure of our conditional, going back to a “natural language” way of saying “if X then Y” — in other words, in the true case, the antecedent finds itself before the consequent:

$$\mathtt{A()\:if\:condition\:else\:B()\equiv}\begin{cases}
A()\leftarrow condition()\\
\neg condition() \rightarrow B()
\end{cases}$$

This nuance might seem nit-picky at first, but here’s an exaggerated example of confusing code you might (and I have) see in the wild:

Now, it’s very easy when reading code like this to forget that there could be an if at the bottom of that expression that (and this is key) completely changes the meaning of what you just read! In fact, Python’s own documentation says that this seems “strange and backwards”:

This syntax may seem strange and backwards; why does the condition go in the middle of the expression, and not in the front as in C’s c ? x : y? The decision was checked by applying the new syntax to the modules in the standard library and seeing how the resulting code read. In many cases where a conditional expression is used, one value seems to be the ‘common case’ and one value is an ‘exceptional case’, used only on rarer occasions when the condition isn’t met.

So not only are statements like z = a if b else c logically backwards and syntactically confusing, PEP 308 is also guilty of being semantically ambiguous. I get it: what we mean when we say return (doc + '\n') if doc else '' is that usually we’ll return a document followed by a newline, but rarely we will return an empty string. But even so, if this is the use case PEP 308 is meant to resolve, why not add the usually keyword, it would make life much easier: z = usually a if b else c. I instantly know, for one, that I’m looking at a conditional. I also know that I’m dealing with a “typical” and a “non-typical” case.

Of course, I’m making the big assumption people use this syntax as it is intended, so let’s take a look at some random example of Python’s conditional expression in a standard library:

According to Python’s own documentation greater_than_half = r > b if b > 0 else r < b means that the remainder of a / b should usually be greater than b. But that is completely absurd: this piece of code doesn't have any a priori knowledge of the kinds of numbers I'm dividing. So that's why PEP 308 sucks: it's semantically ambiguous and it's syntactically awkward. It should've went with x = condition ? a : b

There are many other things I hate about Python — I'm looking at you list comprehension. And you too, lambdas. By the way, both lambdas and list comprehension sometimes break when used with conditional expressions. How fun. But those are topics for another day.