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:

  1. get_age returns None when it does not recognize the name of someone.

  2. get_age returns an integer value when it does recognize the name of someone. However, if their age happens to be 0, then Python converts it to False because bool(0) is False.

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 or False value and try to make it so, that you carefully consider what it maps to. Letting Python figure out what’s True 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 to True or False yourself you should exercise great care.

None is not False

It is infact just None. If your code interacts – as it so often will – with None then you must test for that explicitly with is None or is not None.

The Element object from above perfectly illustrates the danger of not testing for existence with None. If you can make your code explicit and obvious then you should always prefer to do so.

Everything evaluates to True or False

If you cram something through a fine-meshed, boolean sieve you’re going to end up with True or False. 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 that 0 and None are both False 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.

Liked the Article?

Why not follow us …

Be Inspired Get Python tips sent to your inbox

We'll tell you about the latest courses and articles.

Absolutely no spam. We promise!