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.).
$(( 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:
argument by form: Python syntax
argument by effect: Python bytecode
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
: 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 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
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
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 (
And here is an equivalent function using a standard
>>> 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:
(...)*(parentheses and asterisk) for zero or more occurrences of what is contained in the parentheses;
|(pipe) for logical disjunction;
[...](square brackets) for optional content that occurs at most once.
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
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)* ...
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
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
test is more rigid:
test: or_test ['if' or_test 'else' test] | lambdef
else, which is optional in an
if statement. Symbol
test itself resolves ultimately to a
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
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
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
Addendum on quaternary
Note that it has only two instances of letter r — quaternary, 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.