After You. No, I Insist, You Go First • Python's Operator Precedence
Some topics are boring, and Python's operator precedence is one of them. Let's fix that…
This was Dan's dream job. But his first day isn't going well. Let's be honest, it's a disaster.
The day he found out he got the job as the Mayor of PyTown's personal assistant seems so far away. The joy, elation, and pride of that day are replaced by despair, desperation, and dismay as he tries to speed up the clock on the wall by staring at it intensely, hoping to reach the end of this first-day nightmare as quickly as possible.
It all started when the Mayor greeted Dan to welcome him to his new post. Dan was already quite nervous.
"After you", said the Mayor politely, ushering Dan with a shallow wave of his hand.
"You go first, sir", stuttered Dan.
"No, I insist, you go first."
We've all been in a situation like this. Or its alternative form—you'll recognise this: You're walking in town, and your gaze meets that of another pedestrian walking towards you. You both realise you're on a collision course. You both alter direction slightly. But you both shifted your path in the same direction. You smile at each other and alter direction again. But you do so at the same time. The smiles are wider now as you acknowledge this left-right dance. And finally, one of you makes a larger and clearer move out of the way. No harm done.
Dan wasn't so lucky. After a few awkward exchanges, he decided to head through the door…but the Mayor had also decided to step through at the same time. Crash. And the Mayor's coffee, hot and black, spilt all over his suit—the Mayor's suit. Dan was unblemished, which added to his embarrassment.
To put him at ease, the Mayor gave Dan his first task right away.
"Can you get me an orange juice or coffee and milk, please"
Dan was still in a daze. The security guard at the door pointed to his right, which is where the kitchen is. He poured half a glass of orange juice and topped it up with milk.
It didn't click. Dan took this concoction back to the Mayor, and he only realised what he'd done when he saw the Mayor politely pretending he wasn't thirsty anymore.
"Here's something I need to sort out today that you can help with," the Mayor skilfully said, hoping to put Dan at ease.
"Here's the folder with all the town hall employees. Calculate the bonus for each staff member as 10% of the base salary plus overtime hours times the hourly rate, and add any special bonus allowances."
Dan took a deep breath, smiled confidently, took the folder, and reversed out of the Mayor's office with a deference fit for a king. He sat at his desk. He paused. Calculate the bonus as 10% of the base salary plus overtime hours times the hourly rate, and add any special bonus allowances. In a calm mindset, Dan would have guessed what the Mayor meant. But despite Dan's best efforts to convey a zen-like outward calm, there was plenty of turbulence inside. He worked out 10% of everyone's salary. Then, he added the overtime hours' pay and special allowances. The bonus bill was huge since many employees had plenty of overtime! I'll let you guess the Mayor's reaction when he saw the total.
You may recall meeting the Mayor of PyTown in a previous article about local and global variables: The Mayor of Py Town's Local Experiment: A Global Disaster
Operator Precedence
The Mayor could have been clearer when instructing Dan. "Can you get me either an orange juice or else, you can get a coffee and milk instead." I'm sure you often had instances when you asked the speaker for clarification if something they said was ambiguous.
But how does Python deal with these problems? It has a hierarchy of operators and executes those higher up in the list before those lower down.
Let's look at some examples.
Here are some fictional characters from books and films I read or watched recently:
Let's write code that finds all names that start with either B or Y that are shorter than eight characters. To keep this simple, we'll use characters instead of letters, so we'll include the spaces and hyphens in those names that have them.
First attempt
Here's a first attempt:
You create a list selection
using a list comprehension. I split the list comprehension into three lines, which makes it more readable. The final line in the list comprehension is the condition you use to filter the names you want to keep:
Let's look at everything on this line:
It starts with the
if
keyword.It has four names (or identifiers):
name
appears three times, andlen
appears once.It has two pairs of square brackets. In both cases, they follow immediately after a name (identifier).
It has a pair of parentheses right after the name of a function,
len
.It has comparison and equality operators:
==
appears twice and<
appears once.It has an
or
and anand
.It has two string literals,
"B"
and"Y"
, and an integer literal, 8.
Rather than dealing with all of these at once, let's focus on two of these operators, the Boolean operators and
and or
.
Should Python check whether the first letter of the name is B or Y first and then check whether the name is shorter than eight characters? Or should it first check whether the name starts with a Y and is shorter than eight characters, and then check whether it starts with a B?
Python can't read your mind. It doesn't know what your intention is. So, let's figure out what happens by running this code:
['Yoda', 'Bilbo Baggins']
Both names start with either B or Y. So far, so good. But Bilbo Baggins's name has 13 characters, including the space. That's more than eight. Bilbo Baggins shouldn't be in the list.
That's because and
has a higher precedence than or
. Therefore, even though or
appears first in the line of code, Python performs the and
comparison first. Let's drill down into what this means by considering Yoda and Bilbo Baggins.
Let's transform the line of code step by step. Here's the line of code again. The following steps are intended for demonstration purposes only, and they’re not code you should write in your program:
if name[0] == "B" or name[0] == "Y" and len(name) < 8
When name = "Yoda"
, you can picture this line as follows:
if "Y" == "B" or "Y" == "Y" and len("Yoda") < 8
The and
operation occurs first. Therefore, Python checks that "Y" == "Y"
and len("Yoda") < 8
. Both these expressions are True
. The and
expression evaluates to True
:
if "Y" == "B" or True
You're left with the or
expression, which has two operands:
"Y" == "B"
True
The first expression is False
, but the second is True
. Therefore, or
evaluates to True
.
Yoda makes the cut!
Let's repeat the process with Bilbo Baggins, starting from the full line of code:
if name[0] == "B" or name[0] == "Y" and len(name) < 8
When name = "Bilbo Baggins"
, you can visualise the line as follows:
if "B" == "B" or "B" == "Y" and len("Bilbo Baggins") < 8
The first expression to be evaluated is "B" == "Y" and len("Bilbo Baggins") < 8
since and
takes precedence over or
. Both operands are False
since B is not Y and there are too many characters in Bilbo's full name:
if "B" == "B" or False
It's the or
operator's turn now. One of the operands, "B" == "B"
, is True
. Since or
only needs one of its operands to be True
(or truthy), the whole or
expression evaluates to True
, even though the second operand is False
.
Bilbo Baggins also makes the cut. But this is not the desired result.
Second attempt
We started building the operator precedence hierarchy. So far, you determined that and
comes above or
. Let's shoot to the top of the hierarchy to find a tool to use in your second attempt. Parentheses are at the very top. They take precedence over everything else. Other types of brackets also have the same priority, such as []
to create lists and {}
to create dictionaries or sets.
So, if you want or
to leapfrog and
in the order of operation, you can enclose the or
expression in parentheses. The parentheses pull this expression to the top of the precedence hierarchy:
Note the additional parentheses in the if
clause. Here's the output now:
['Yoda']
Yoda is still there, but Bilbo Baggins isn't. Let's see why Yoda makes the cut using the same technique as in the previous section:
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
When name = "Yoda"
:
if ("Y" == "B" or "Y" == "Y") and len("Yoda") < 8
The expression in the parentheses runs first, and since one of the operands is True
, the or
expression evaluates to True
:
if True and len("Yoda") < 8
And Yoda's name contains only four characters, so both expressions evaluate to True
in the and
expression. Yoda is in the list.
How about Bilbo Baggins. Let's start from the beginning:
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
Now, name = "Bilbo Baggins"
:
if ("B" == "B" or "B" == "Y") and len("Bilbo Baggins") < 8
Since the parenthesised or
expression comes first, you get:
if True and len("Bilbo Baggins") < 8
The or
expression is True
since one of the operands is True
. But Bilbo Baggins has a long full name, so the second operand in and
is False
. Therefore, the and
expression evaluates to False
. Bilbo is not in the list.
What Can We Say About the Precedence of the Other Operators?
Let's revisit the line containing the if
clause in the list:
You've seen how parentheses used to group expressions are at the top of the precedence order. Square or curly brackets to group elements in a list, dictionary or set are also at the same level. The second entry in the hierarchy of operator precedence also includes parentheses and square brackets, but these are the ones that follow immediately after an object to call a function or class or to subscript an object (such as indexing, slicing or fetching a value from a dictionary).
Back to our line of code: In the expression in parentheses, the two occurrences of indexing, name[0]
, are the first operations to be executed. This may seem obvious, but it's a result of these operations coming very high up in the precedence list.
And if you're familiar with using or
, you can infer that the equality operator ==
is higher than or
in the order of precedence. If this weren't the case, the code in the parentheses would behave like this expression: name[0] == ("B" or name[0]) == "Y"
. This expression always evaluates to False
. The string "B"
is not empty, and therefore, it's truthy. The or
expression always evaluates to the first operand if it's truthy. But "B" == "Y"
is always False
!
Fast forward to when the program looks at the second operand in the and
expression, which is len(name) < 8
. Since the parentheses after the identifier len
are second only to standalone parentheses (or other brackets) in the precedence list, and there are no standalone parentheses in this expression, then calling the function comes before the <
operator. Right, this doesn't come as a surprise, I know.
Here's the order of operator precedence you've discovered so far. I'm numbering the list, but note that there are gaps in the numbers. That's not a typo! There are other operators we'll need to fill in later—but I won't try to deal with all of them in this article. Otherwise, we're here forever!
1. Parentheses to group expressions and square and curly brackets to create lists, dictionaries, and sets:
(expressions...)
,[expressions...]
,{key: value}
,{expressions...}
2. Parentheses and brackets to call, subscript, and slice. The dot notation to access object attributes shares the same hierarchical level:
obj[index_or_key]
,obj[start:stop]
,some_function(arguments...)
,some_object.attribute
12. Equality and comparison operators. Membership tests and identity tests are also at this level:
in
,not in
,is
,is not
,<
,<=
,>
,>=
,!=
,==
14. The
and
operator15. The
or
operator
Dan, The Mayor, and Python's Precedence Rules
"Can you get me an orange juice or coffee and milk, please"
If the Mayor's office followed Python's operator precedence rules, where and
comes before or
, Dan wouldn't have made any errors.
How about the second request from the Mayor?
"Calculate the bonus as 10% of the base salary plus overtime hours times the hourly rate, and add any special bonus allowances."
In Python, this would translate to:
This line includes two operators: *
and +
. As is common in mathematics, multiplication takes precedence over addition. This leads to the same error Dan made: the overtime pay is added in full to the bonus, which is not good for the town hall's treasurer. The Mayor meant to say that Dan should first add the base salary and the overtime pay and then take 10% of the sum. Finally, he can add the special bonus allowances.
There's a reason why parentheses are at the top of the precedence list. They're useful in these situations when you need to modify the order of operations:
The parentheses ensure that the base salary is added to the overtime pay first before multiplying by 0.1. There's no need to put overtime_hours * hourly_rate
in additional parentheses since *
comes before +
in the hierarchy.
I'm Curious Now: What's the Full Operator Precedence List?
No self-respecting article on operator precedence can conclude without showing the full table of operator precedence, right? But I don't do "self-respecting", clearly. I'd just be copying the table from the documentation, so here's the link to the table directly in Python's docs instead of presenting it here:
I’m running a technical writing workshop later this month. It’s a two-hour live Zoom session: From Core Principles to Storytelling Techniques in Technical Writing
Conclusion
Dan and the Mayor had a good chat at the end of the day. They both realised they needed to improve how they communicated with each other to remove ambiguity.
Maybe they should both keep a printout of Python's operator precedence table on their desks for reference!
Code in this article uses Python 3.12
Stop Stack
#67
I'm making a number of free passes available for readers of The Stack to access the learning platform at The Python Coding Place. The passes give access to all the video courses for one week and a live session, similar to the ones I have weekly with members of The Python Coding Place. Here's the link to get your one week's pass:
Thank you to all those who supported me with a one-off donation recently. This means a lot and helps me focus on writing more articles and keeping more of these articles free for everyone. Here's the link again for anyone who wants to make a one-off donation to support The Python Coding Stack:
The Python Coding Book is available (Ebook and paperback). This is the First Edition, which follows from the "Zeroth" Edition that has been available online for a while—Just ask Google for "python book"!
And if you read the book already, I'd appreciate a review on Amazon. These things matter so much for individual authors!
And for those who want to join The Python Coding Place to access all of my video courses—past and future—join regular live sessions, and interact with me and other learners on the members-only forum, here's the link:
Any questions? Just ask…
Appendix: Code Blocks
Code Block #1
characters = [
"Jean-Luc Picard",
"Harry Potter",
"Sherlock Holmes",
"Yoda",
"Bilbo Baggins",
"Katniss Everdeen",
]
Code Block #2
# ...
selection = [
name
for name in characters
if name[0] == "B" or name[0] == "Y" and len(name) < 8
]
Code Block #3
if name[0] == "B" or name[0] == "Y" and len(name) < 8
Code Block #4
characters = [
"Jean-Luc Picard",
"Harry Potter",
"Sherlock Holmes",
"Yoda",
"Bilbo Baggins",
"Katniss Everdeen",
]
selection = [
name
for name in characters
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
]
print(selection)
Code Block #5
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
Code Block #6
0.1 * base_salary + overtime_hours * hourly_rate + special_allowances
Code Block #7
0.1 * (base_salary + overtime_hours * hourly_rate) + special_allowances
Rule #5: Always use parentheses!