Boolean Operators and Short-Circuiting
by Christoph Schiessl on Python
Recently, I have written about various topics related to Booleans in Python, such as the built-in any()
/ all()
functions and the Standard Truth Testing Procedure. But, what I did not yet explain about are three boolean operators themselves:
- Negation with the
not
keyword, - Conjunction with the
and
keyword, and - Disjunction with the
or
keyword.
Negation with the Unary not
Operator
The not
operator is the simplest of all the boolean operators. Firstly, it's a unary operator, which takes only a single operand as input. Secondly, the operator's evaluation result is always True
or False
(i.e., an instance of the bool
class). Thirdly, there is no short-circuiting behavior — the whole concept isn't even applicable to unary operators.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class IAmTrue:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmTrue(label='{self._label}')"
... def __bool__(self):
... print("IAmTrue.__bool__() has been called.")
... return True
...
>>> class IAmFalse:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmFalse(label='{self._label}')"
... def __bool__(self):
... print("IAmFalse.__bool__() has been called.")
... return False
...
>>> not IAmTrue('1st')
IAmTrue.__bool__() has been called.
False
>>> not IAmFalse('1st')
IAmFalse.__bool__() has been called.
True
Conjunction with the Binary and
Operator
Next up is the and
operator, which is more interesting because it's a binary operator, meaning it takes two operands as input. The result is that expressions like x and y
evaluate to x
if and only if x
converts to False
; otherwise, they evaluate to y
. This behavior is known as short-circuiting because — as long as the first operand converts to False
— it doesn't even consider the second operand. Furthermore, note that, in all cases, only the first operand is ever truth-tested!
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class IAmTrue:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmTrue(label='{self._label}')"
... def __bool__(self):
... print("IAmTrue.__bool__() has been called.")
... return True
...
>>> class IAmFalse:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmFalse(label='{self._label}')"
... def __bool__(self):
... print("IAmFalse.__bool__() has been called.")
... return False
...
>>> IAmFalse('1st') and IAmFalse('2nd') # short-circuit to 1st
IAmFalse.__bool__() has been called.
IAmFalse(label='1st')
>>> IAmFalse('1st') and IAmTrue('2nd') # short-circuit to 1st
IAmFalse.__bool__() has been called.
IAmFalse(label='1st')
>>> IAmTrue('1st') and IAmFalse('2nd')
IAmTrue.__bool__() has been called.
IAmFalse(label='2nd')
>>> IAmTrue('1st') and IAmTrue('2nd')
IAmTrue.__bool__() has been called.
IAmTrue(label='2nd')
Disjunction with the Binary or
Operator
Last but not least is the or
Operator, also a binary operator. The result is that expressions like x or y
evaluate to x
if and only if x
converts to True
; otherwise, they evaluate to y
. This is again short-circuiting behavior: As long as the first operand converts to True
, it doesn't even consider the second operand. Furthermore, note that, in all cases, only the first operand is ever truth-tested!
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class IAmTrue:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmTrue(label='{self._label}')"
... def __bool__(self):
... print("IAmTrue.__bool__() has been called.")
... return True
...
>>> class IAmFalse:
... def __init__(self, label): self._label = label
... def __repr__(self): return f"IAmFalse(label='{self._label}')"
... def __bool__(self):
... print("IAmFalse.__bool__() has been called.")
... return False
...
>>> IAmFalse('1st') or IAmFalse('2nd')
IAmFalse.__bool__() has been called.
IAmFalse(label='2nd')
>>> IAmFalse('1st') or IAmTrue('2nd')
IAmFalse.__bool__() has been called.
IAmTrue(label='2nd')
>>> IAmTrue('1st') or IAmFalse('2nd') # short-circuit to 1st
IAmTrue.__bool__() has been called.
IAmTrue(label='1st')
>>> IAmTrue('1st') or IAmTrue('2nd') # short-circuit to 1st
IAmTrue.__bool__() has been called.
IAmTrue(label='1st')
Operator Precedence
Finally, we have to talk about precedence. Here are the three boolean operators ordered from highest to lowest precedence:
(1) not
, (2) and
, (3) or
. As always, I want to demonstrate what I'm saying ... so, to prove that not
has higher precedence than and
/or
, we need two boolean variables:
for x in [True, False]:
for y in [True, False]:
# Proof: `not` has higher precedence than `and`
assert (not x and y) == ((not x) and y)
assert (x and not y) == (x and (not y))
# Proof: `not` has higher precedence than `or`
assert (not x or y) == ((not x) or y)
assert (x or not y) == (x or (not y))
And three variables to prove that and
has higher precedence than or
:
for x in [True, False]:
for y in [True, False]:
for z in [True, False]:
# Proof: `and` has higher precedence than `or`
assert (x and y or z) == ((x and y) or z)
assert (x or y and z) == (x or (y and z))
If you run these two scripts, you don't get any AssertionError
s, proving that precedence works, as I said before. Anyway, that is everything for today! Thank you for reading, and see you soon!