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 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 one article per week.


I send one email per week 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 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

Encapsulation: Public, Protected, and Private Members

Learn about encapsulation in Python. This article explains public, protected, and private members and the conventions used to implement them.

By Christoph Schiessl on Python

Christoph Schiessl

Hi, I'm Christoph Schiessl.

I help you build robust and fast web applications.


I'm available for hire as a freelance web developer, so you can take advantage of the more than a decade of experience I have collected working on many projects across several industries. Most of my clients are building web-based SaaS applications in a B2B context and depend on my expertise in various capacities.

More often than not, my involvement includes hands-on development work using technologies like Python, JavaScript, and PostgreSQL. Furthermore, if you already have an established team, I can support you as a technical product manager with a passion for simplifying complex processes. Lastly, I'm also an avid writer and educator who takes pride in breaking down technical concepts into the simplest possible terms.