Avoiding the hazards of Python truthy values

Martin McBride
2022-07-20

A truthy value is a value that is considered True when used in a boolean context. A falsy value is a value that is considered False when used in a boolean context. This is a useful feature of Python and several other languages.

They are very useful for writing concise and uncluttered code. But they come with a hidden danger to trap the unwary.

These are falsy values:

  • A zero number (int, real or complex).
  • A zero-length string, list, tuple, dictionary or set.
  • The None object.
  • The False object.

These are truthy values:

  • A non-zero number (int, real or complex).
  • A non-zero length string, list, tuple, dictionary or set.
  • The True object.
  • Almost all other objects that aren't in the falsy list.

Using truthy values

Here are some examples of how truthy values can make your code a little clearer. Consider this code:

n = 1

if n != 0:
    print("n is not zero")

This can be made a little neater using truthy values. If n is not zero it counts as true, so the if clause will be executed:

n = 1

if n:
    print("n is not zero")

Here is some code that only prints a list if it is not empty:

k = [...]

if len(k)!=0:
    print(k)

This can also be tidied up using truthy values. If the list is not empty, it will count as true so the list will be printed:

k = [...]

if k:
    print(k)

Finally, this code will prompt the user to enter their name, but only if the variable name is empty:

name = ...

if len(name)==0:
    name = input("What is your name?")

This code does the same thing using truthy values:

name = ...

name = name or input("What is your name?")

To understand how this works, notice that the code uses an or expression, with two values name and input(...).

The code first evaluates name. If name is true (ie a non-empty string), then Python immediately knows that the or expression will be true, because true or-ed with anything is true.

Python doesn't need to call the input function because it already knows that the result will be true. In fact, Python guarantees that it won't evaluate the second expression if the first one is true. This is called [short-circuit evaluation(https://pythoninformer.com/python-language/intermediate-python/short-circuit-evaluation/). Python will just immediately return the truthy value it has - the initial value of the name variable.

What is name is initially an empty string? Well in that case Python doesn't know whether the or expression will be true or false, so it must evaluate the second part of the or expression. This means that if name is empty it must call the input function. The user will be prompted to enter their name, and whatever they type in will be returned as a truthy value and assigned to name.

Truthy values may take a bit of getting used to if you haven't met them before, but they are generally a good thing. They make your code less cluttered and often avoid unnecessary comparisons or calls to len.

The dark side

Consider this code:

import math

def checked_sqrt(x):
    try:
        return math.sqrt(x)
    except ValueError:
        return None

answer = checked_sqrt(4)

if answer:
    print(answer)
else:
    print("Error calculating square root")

We define a function checked_sqrt that calls the math.sqrt function to calculate a square root.

However, the math.sqrt function will throw a ValueError if x is negative because negative numbers don't have a square root. Our function handles this case by returning None to indicate that no value could be calculated.

We then call checked_sqrt, and check the answer. If we call the function with a value of 4, the function will calculate the square root as 2.0, which is a truthy value, so the if statement will print the answer.

If we call checked_sqrt with a value of -4, an exception will occur and the function will return None. That is a falsy value, so the if statement will print an error message.

That is all fine. But what if we pass in a value of 0? math.sqrt will calculate the square root, which of course is 0.0. Since there was no exception, checked_sqrt will return the value of 0.0.

But when we execute the if statement, answer is 0.0, which is also a falsy value (just line None). This means that our code will incorrectly print the error message even though the square root has been calculated correctly.

Analysis

So what is the problem here? The basic problem is that checking the truthy value of answer cannot distinguish between the cases of None and 0.0.

The deeper problem is that Python uses dynamic typing, which means that checked_sqrt can return different types of data (a float or a None object) under different circumstances. If we tried this code in Java, for example, checked_sqrt would be declared to return a float so returning null would not be possible (it would give a compiler error).

This is compounded by code misdirection. The function explicitly returns None, but the fact that it could also return a different falsy value of 0.0 is not immediately obvious. A quick glance at the code might have you convinced (wrongly) that None is the only possible falsy return value.

The solution

Firstly, it is worth remembering that, in many cases, this will not be a problem. For example, if you have a local variable that is declared with a string value, and is never used in a way that could possibly result in it holding anything other than a string value, then it is perfectly OK to use its truthy value to test if it is empty.

This is true if the variable accepts a return value from a function that only returns a string value. It is true if the value is a parameter of the function that should only ever be a string value. For the purposes of testing the string is empty, the value is supposed to be a string, so it is valid to use its truthy value. The value is not supposed to be None so you can ignore that possibility.

If you are concerned that the value might not, in fact, be a string, then that is a separate issue. That should be enforced using type hints, runtime type checks, unit tests, code reviews and so on. But in the main body of your function, you should assume that the value is of the type it is supposed to be.

So this problem only applies in situations where a value might legitimately have different types. The example above, where the variable can either valid a valid value of None, is a very typical situation.

One solution is very simple. Don't use truthy variables for these cases. Instead, explicitly check if the value is None:

if answer is not None:
    print(answer)
else:
    print("Error calculating square root")

The solution is easy, the tricky part is training yourself to always be on the lookout for this type of problem.

The second solution is to avoid the problem altogether by not allowing the function to return an ambiguous result. For example:

  • The function could always return a number, but that number could be >=0 to indicate a valid result, or -1 to indicate an invalid result.
  • The function could always return a tuple. (True, value) would indicate that the value is valid. (False, value) would indicate that there is no valid result, and value should be ignored.
  • In a similar vein, use optionals.

An optional is a special object that can hold a value that may or may not be present. It includes methods to check if the value is present, to get the value if it is present, and other useful features. It is basically a cleaner alternative to the informal value-or-none protocol.

Summary

Truthy values are a very useful feature of Python, but they can create ambiguity and bugs in certain circumstances. This article included several solutions to this problem that may be applied depending on the specific circumstances.

Popular tags

ebooks fractal generative art generativepy generativepy tutorials github koch curve l systems mandelbrot open source productivity pysound python recursion scipy sine sound spriograph tinkerbell turtle writing