Python's Implicit Booleaness

Beautiful is better than ugly.

Explicit is better than implicit.

…

If that isn’t the quote from the famous The Zen of Python, by Tim Peters. You can see the full commandments if you type:

>>> import this

in any Python command interpreter.

As much as I want to believe these rules are coherent, they can be conflicting with each other under circumstances. This implicit booleaness / emptiness check would be a prime example: beauty clearly outweighs explicitness here.

Context

In full disclosure, I have played with this quite some time ago but recently encountered again, so I decided to ramble a bit in a post. Here it goes.

Say if you want to check if an abject is depleted/empty or true/false to trigger some logic, it’d be naturally to do any of the following:

#!/usr/bin/env python3

# An object that could be a container like list, 
# or just an object instantiated from your own class definition etc
if len(obj) == 0:
    # Do something when the obj is empty
    pass

if obj is None:
    # Do something for null
    pass

while len(obj) > 0:
    # If the length is greater than 0, which implies True
    pass

while len(obj):
    # Implies len(obj) is not 0, which is a broader case of len(obj) > 0.
    # This reminds me of the good times of ancient C, we'd actually -
    # use 0 to represent False cuz the lack of good boolean support
    pass

while not obj.is_empty():
    # If such method/api is provided
    pass

But a more Pythonic and elegant way is just to do:

if not obj:
    # Do something if obj is empty/False/None
    pass

while obj:
    # Continue doing stuff, while obj is not_empty/True:
    pass

Python’s built-in objects would have baked-in supports for this already. According to its documentation1, here are most of the built-in objects considered false:

  1. constants defined to be false: None and False.
  2. zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  3. empty sequences and collections: ‘’, (), [], {}, set(), range(0)

But what if you want to write a class/collection of your own, and would like this groovy feature too, cuz y’all wanna hang out with the cool kids right?

The Experiments

A helper to get started so we don’t keep writing print statements:

def check_bool(obj):
    """
    Emptiness should be equivalent to False
    """
    if obj:
        print('object is True or not empty, and bool(obj) is: ', bool(obj))
    else:
        # aka `not obj`. 
        print('object is False or empty!! bool(obj) is: ', bool(obj))
    print()

Let’s begin by a dummy class without any actual implementation in it:

class Dummy:
    pass

and do:

obj = Dummy()
check_bool(obj)

This turned out to be True aka not empty, which is a bit counter-intuitive. That’s cuz when it’s evaluated for booleaness, Python would do bool(obj) which then triggers the built-in magic method __bool__() if available. So if we do the following instead:

class BoolDummy(Dummy):
    def __bool__(self):
        return False


obj = BoolDummy()

# it will be False now
check_bool(obj)

Then this new dummy object would now be evaluated as false.

But just having __bool__() return True/False outright seems a bit arbitrary. We could take a step further and tie it to the emptiness, as shown in the homemade list container/collection example below (obviously there could be other boolean check use cases as well):

class ContainerBase:
    """A copycat of Stack/List"""

    def __init__(self):
        self.container = []

    def push(self, item):
        self.container.append(item)

    def pop(self):
        self.container.pop()

class HomemadeContainer(ContainerBase):
    def __init__(self):
        super().__init__()

    def __bool__(self):
        return len(self.container) != 0

Now it’ll behave just like what a built-in list would do, in terms of emptiness/False:

obj = HomemadeContainer()

# False since it's empty to begin with
check_bool(obj)

obj.push('a')

# True now that obj is not "empty"
check_bool(obj)

Alternatively, we could also resort to another implicit check for len(obj) that achieves the same effect, which means overriding Python’s magic method of __len__(), here’s an example:

class AltHomemadeContainer(ContainerBase):
    def __init__(self):
        super().__init__()
    
    def __len__(self):
        return len(self.container)

obj = AltHomemadeContainer()

assert len(obj) == 0

# False since it's empty to begin with
check_bool(obj)

obj.push('a')

# should be 1 now
assert len(obj) == 1

# True now that obj is not "empty"
check_bool(obj)

Now we can totes do shorthands like if obj with it! Not half bad eh?

While you can get away with overriding either __bool__() or __len__(), there’s absolutely no harm in doing both (I’d argue len is actually quite useful). Why not get the best of both worlds when you can! :D


  1. Python doc on Truth Value Testing. ↩