There is no ternary operator in Python

& (verbiage overflow)Sat 30 April 2016RSS

In this post I describe the “ternary operator” and argue that although people refer to a certain Python structure by this name, in reality Python doesn’t have a ternary operator at all.

What does it mean to be “ternary”?

“The ternary operator” usually means a specialized if...else... construction, first introduced in C:

condition ? if_true : if_false

(Kernighan and Ritchie, The C Programming Language, second edition, secs. 2.11, A7.16.)

The word “ternary” itself is well known — maybe best known in the tech community — in this usage. It isn’t limited to computer science — it just means “three-part” or “of order 3”, and it corresponds to unary for “one-”, binary for “two-”, and quaternary for “four-” in the same senses. An important usage of ternary other than in programming is in music: ternary measure: ‘triple time’ (a time signature of 3/4, 3/8, etc.).

In programming we are really talking about what we should call a “two-way decision expression”. In C this expression is “an operator” because it is a single entity, even though it has two parts that always appear separated by one of the arguments. Granted that the format is anomalous, many languages have adopted it: JavaScript, Java, Ruby, Bash, PHP. Here is Bash, for instance:

$(( condition ? if_true : if_false ))

This is the only C operator that requires three arguments — we don’t bother using ternary to describe functions that take three arguments, or series of keywords acting on three expressions.

Does Python have something comparable?

Arguments against a Python ternary operator

Below I make three arguments against the idea that Python has a comparable structure:

  1. argument by form: Python syntax

  2. argument by effect: Python bytecode

  3. argument by design: Python expression grammar

Argument by form: Python syntax

In C, significantly, the element ? isn’t otherwise used as an operator. In particular, it doesn’t otherwise have the meaning if. Operator : appears only in an unrelated use (declaring the bits assigned to a field in a struct). Together,

? :

in C constitutes a single operator that cannot reasonably be confused with anything else.

Contrast the situation in Python. The Python structure that many people refer to as a ternary operator is not an operator at all — it is an expression, and in terms of its superficial form it is just a variety of if-statement syntax, differing in the placement and order of its keywords:

x = True if random.randint(0, 1) else False

That is an inline equivalent to this ordinary if...else... statement:

if random.randint(0, 1):
    x = True
else:
    x = False

Such a structure does not deserve the name “ternary” because it is not a single operator — it is a series of keywords.

Question: Does the rearranged syntax mean that, internally, Python handles it differently from a standard if...else... statement?

Argument by effect: Bytecode instructions in CPython

To answer that, we can use the dis module to examine empirically the bytecode instructions into which a two-way decision expression is parsed, and compare it with traditional if...else....

Here is a simple example:

>>> import random
>>> def if_else():
...     return True if random.randint(0, 1) else False
...
>>> dis.dis(if_else)
  2           0 LOAD_GLOBAL              0 (random)
              3 LOAD_ATTR                1 (randint)
              6 LOAD_CONST               1 (0)
              9 LOAD_CONST               2 (1)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 POP_JUMP_IF_FALSE       22
             18 LOAD_CONST               3 (True)
             21 RETURN_VALUE
        >>   22 LOAD_CONST               4 (False)
             25 RETURN_VALUE

We can see that the function loads randint, chooses 0 or 1 at random, and then based on the result either jumps to address 22 (return False) or continues to address 18 (return True).

And here is an equivalent function using a standard if...else... block:

>>> def if_else_standard():
...     if random.randint(0, 1):
...         return True
...     else:
...         pass
...     return False
...
>>> dis.dis(if_else_standard)
  2           0 LOAD_GLOBAL              0 (random)
              3 LOAD_ATTR                1 (randint)
              6 LOAD_CONST               1 (0)
              9 LOAD_CONST               2 (1)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 POP_JUMP_IF_FALSE       22

  3          18 LOAD_CONST               3 (True)
             21 RETURN_VALUE

  4     >>   22 LOAD_CONST               4 (False)
             25 RETURN_VALUE

The sequence of instructions — opcodes, instruction-addresses, and arguments — is identical. These are the same function, apart from the trivial disparity of how many lines the original code is spread over.

Question: Granted that the if...else... statemnt and the two-way decision expression are fundamentally the same in syntax and underlying bytecode instructions, does the parser travel from syntax to bytecode by the same path in both cases?

Argument by design: Python grammar

To answer that, we can look at the Python expression grammar — the roadmap describing how Python is parsed. An expression grammar is a series of lines — on the left part of the line is a single symbol, followed by a colon; to the right of the colon is some expression or series of expressions that can be decomposed into keywords (in quotes), symbols, and a few basic operators:

Python today uses a symbol test for basic conditional evaluation, beginning in v. 2.6. Here is a pertinent snippet of the expression grammar defining if and the test symbol:

if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
...
test: or_test ['if' or_test 'else' test] | lambdef
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
...

(Symbols suite, lambdef, shift_expr, and subsequent replacement expressions are omitted for simplification.)

Notice is that the if...[elif...]else... statement is now a kind of wrapper around conditional evaluation with test:

if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]

Interestingly, test is more rigid:

test: or_test ['if' or_test 'else' test] | lambdef

It requires else, which is optional in an if statement. Symbol test itself resolves ultimately to a comparison expression

comparison: expr (comp_op expr)*

In other words, in Python a two-way decision expression is not a distinct structure from an if statement at all.

So there is simply no “ternary operator” in Python.


Addendum: How the two-way decision statement got into Python

Symbol test had initially been introduced along with testlist in v. 2.3 for support of filtering (without an else option) in list comprehensions, already in existence since v. 2.0.

Actual two-way decision expressions entered Python in the rule for the expression symbol, between versions 2.4:

expression ::= or_test | lambda_form

and 2.5:

expression ::= or_test [if or_test else test] | lambda_form

In versions 2.5.1 through 2.5.4 this rule was amplified to:

expression ::= conditional_expression | lambda_form
...
conditional_expression ::= or_test ["if" or_test "else" expression]

But in v. 2.6 the symbol test was restored, to replace conditional_expression, and that is how things remain in v. 3.5.

In v. 2.6, symbol comparison now pointed to expr, rather than or_expr, as it had from v. 1.4 onward; expr now pointed to xor_expr. What that means is that the test symbol that ultimately supported if statements until v. 2.6 was founded on logical OR.

Addendum on quaternary

Note that it has only two instances of letter rquaternary, not *quaRternary.

Before you ask: quinary: ‘five-part’ or ‘of order 5’. Sexenary, septenary, octonary, nonary/novenary, and decenary are also attested. Vicenary/vigenary/vigesimal for twenty, triacontad for thirty, centenary for one hundred, millenary for one thousand. Stop me, someone.

[end]

Comments are enabled.



All prior articles

  1. Does Fortran have a two-way decision function?