on
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:
- constants defined to be false: None and False.
- zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
- 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