Truthy and Falsy Gotchas
Think back to when you wrote your first ever if
statement in Python. I’m sure that intuition told you to only give Python boolean expressions that naturally evaluates to True
or False
, like 2 + 2 == 4
.
But sooner, rather than later, you find yourself testing a list or a string’s length because it’s once again intuitive to you and, in other programming languages, possibly the only way to do so:
items = [1, 2]
if len(items) > 0 or items != []:
print(f'There are {len(items)}')
else:
print('There are no items')
Before long, however, you learn that it’s un-Pythonic: that there’s a better way, a shorter way. You learn that Python will evaluate just about anything in a boolean context given the opportunity and if you squint your eyes it all makes sense.
A boolean context is anything where Python expects nothing but True
or False
, such as an if
or while
statement.
What is Truthy and Falsy?
items = [1, 2]
if items:
print(f'There are {len(items)}')
else:
print('There are no items')
The terms truthy and falsy refer to values that are not ordinarily boolean but, when evaluated in a boolean context – like inside an if
statement – they assume a True
or False
value based on characteristics inherent to the value of the object you are giving it. For example: the number zero, empty strings and lists evaluate to False
; but non-zero numbers, non-empty lists and lists are True
.
In this example Python knows that items
is a list and that it is truthy if it contains at least one item, and that it is falsy if it contains zero. There’s no need to check if the list is equal to an empty list, or that it has a length of 0. Python “does the right thing.”
But what about None
?
Ah. Yes. Well it evaluates to False
, so that’s the end of that, right:
>>> bool(None)
False
Weeell, no. So it just evaluates to False — like an empty list or string, or the number zero. So if you test an expression in an if
statement and it is None
it gets turned to False
and the world keeps ticking and nothing bad ever happens.
1# age.py
2AGES = {
3 "Bob": 42,
4 "Selma": 0,
5}
6
7
8def get_age(name):
9 # If there is no one named ``name``, return None.
10 return AGES.get(name)
11
12
13person = "Selma"
14age = get_age(person)
15if age is None: # if not age:
16 print(f"There is no one with the name {person}")
17else:
18 print(f"{person}'s age is {age}")
In this (slightly contrived) example there’s a function get_age
, that returns the age of a person it knows about. If it does not find anyone with that name it returns None
.
In other words, get_age
has a return type of Optional[int]
. Meaning, it can be either: an integer value and the age of the person; or None
, to indicate that no such person exists.
But why test for is None
as I do in the if
-statement if the shorthand falsy check works fine? A naive interpretation of idiomatic Python discourages you from explicitly testing for falsy or truthy values with the understanding that it will sort it out for you. But let’s run the code and observe what happens:
$ python age.py Selma's age is 0
Which is correct. Selma is not yet 1 year of age. But if I replace the if age is None
check with if not age
:
$ python age.py There is no one with the name Selma
And now our application has a subtle logic error. The reason is that we accept multiple falsy values but treat them as though they were one:
-
get_age
returnsNone
when it does not recognize the name of someone. -
get_age
returns an integer value when it does recognize the name of someone. However, if their age happens to be0
, then Python converts it toFalse
becausebool(0)
isFalse
.
The mistake here is coalescing two distinct conditions (None
and 0
) and treating them as though they were one. This is a very common anti-pattern in Python that you see repeated everywhere. Even when where there is, admittedly, no risk of mistakes:
import re
m = re.match(r'\d', '1234')
if m:
# there is a match ...
else:
# ... no match
The re.match
function returns either a match object or None
, so there is no risk of coalescing two distinct falsy values here. But it’s not correct; it just so happens that it works fine. You should mind the None
values that you encounter, but it is something more honored in the breach than in the observance.
I am going to end this with an actual example of where your intuition and blindly relying on Python to do the right thing will put you in hot water:
1# etree.py
2import xml.etree.ElementTree as ET
3
4# Parse this XML snippet into a Document Object Model
5dom = ET.fromstring("<Greeting>Hello, <Name>World</Name></Greeting>")
6
7# Find the ``Name`` element in the DOM.
8element = dom.find("Name")
9missing_element = dom.find("Missing")
10
11if not element:
12 print("The element does not exist!")
13else:
14 print("The element *does* exist")
15
16# But the element exists and we can read its text.
17assert element.text == "World"
18
19
20# But...
21print("Element bool value: ", bool(element))
22print("Element is None: ", element is None)
23print("Missing element bool value: ", bool(missing_element))
24print("Missing element is None: ", missing_element is None)
This is a simple example that generates a little XML document and then reads two elements from it: one that exists and one that is missing.
If you run the code you get some surprising results:
$ python etree.py The element does not exist! Element bool value: False Element is None: False Missing element bool value: False Missing element is None: True
The element that exists evaluates to False
– as the condition for it ever being True
is when it has one or more child elements – but the missing element is None
which is also False
(in a boolean context). The only way to test that the element is missing is with a None
check because dom.find()
returns None
if it cannot find the missing element.
And therein lies the danger of falsy checking. Depending on your viewpoint the ElementTree
works as intended: an Element
is only ever evaluated as True
if, and only if, it has at least one child element. For all other conditions it is False
. So the proper way to check whether an element exists is with is not None
.
One last remark. If you use this library then you must not write if
statements that depend on Element
returning True
only if it has children. Another developer – unaware of this unintuitive behavior – may instead assume it checks for existence.
Summary
- Using truthy and falsy checks is perfectly fine
-
But you must consider that if you take something that is inherently not a
True
orFalse
value and try to make it so, that you carefully consider what it maps to. Letting Python figure out what’sTrue
or not is almost always fine – notwithstanding the XML example above – and unlikely to get you into trouble, but if you have to interface with poorly-written third-party libraries or if you are tasked with defining whether a compound object maps toTrue
orFalse
yourself you should exercise great care. None
is notFalse
-
It is infact just
None
. If your code interacts – as it so often will – withNone
then you must test for that explicitly withis None
oris not None
.The
Element
object from above perfectly illustrates the danger of not testing for existence withNone
. If you can make your code explicit and obvious then you should always prefer to do so. - Everything evaluates to
True
orFalse
-
If you cram something through a fine-meshed, boolean sieve you’re going to end up with
True
orFalse
. But that does not mean it’ll give you the intuitive answer you like. You’re trading the state of a possibly-complex object with something that is rather binary. Make sure you understand what that trade-off implies.The
get_age
example taught you that0
andNone
are bothFalse
yet one represents the absence of age and the other a literal age. And the XML snippet taught you that your intuition may not match the intent of the developers who wrote the code in the first place.