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 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 AssertionErrors, proving that precedence works, as I said before. Anyway, that is everything for today! Thank you for reading, and see you soon!

Ready to Learn More Web Development?

Join my Mailing List to receive two useful Articles per week.


I send two weekly emails on building performant and resilient Web Applications with Python, JavaScript and PostgreSQL. No spam. Unscubscribe at any time.

Continue Reading?

Here are a few more Articles for you ...


The Built-in bool() Class

Learn about boolean values in Python and the standard truth testing procedure. Understand how objects are converted to True or False.

By Christoph Schiessl on Python

The Built-in callable() Function

Learn about the callable() function in Python. This article explains how everything in Python is potentially callable, including classes and instances.

By Christoph Schiessl on Python

The Built-in any() Function

Learn how to use the built-in any() function in Python to determine if any element in an iterable is True, with implementation and performance insights.

By Christoph Schiessl on Python

Christoph Schiessl

Christoph Schiessl

Independent Consultant + Full Stack Developer


If you hire me, you can rely on more than a decade of experience, which I have collected working on web applications for many clients across multiple industries. My involvement usually focuses on hands-on development work using various technologies like Python, JavaScript, PostgreSQL, or whichever technology we determine to be the best tool for the job. Furthermore, you can also depend on me in an advisory capacity to make educated technological choices for your backend and frontend teams. Lastly, I can help you transition to or improve your agile development processes.