Getting a Boolean from any value in Python
Posted on Fri 14 July 2023 in Python
Getting a Boolean value from any data type
Recently, I was building a webhook that needed to accept and operate on varied data from outside sources. Included in that data were values meant to be Booleans. However, some callers were sending values like "Yes"
or "no"
or even "-1"
. I needed to convert each of those to a Boolean value based on their semantic meaning. In other words, we humans know that the string "false" should represent a False
value. But on it's own, Python would treat the non-empty string as a True
value.
I'll give background and explanation below. But for the impatient readers out there, here's the code I came up with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
In the actual project, I added a suite of tests around this function. For simplicity in this post, let's just manually check some values.
truthy_values = [True, "True", "true", "TRUE", "Yes", 1, "1", 0.1, {"a": False}]
falsey_values = [False, "False", "false", "FALSE", "No", False, "To be or not to be", 0, "0", -1, None, {}]
for val in truthy_values:
print(f"`{val}` is {get_bool_from_any(val)}")
for val in falsey_values:
print(f"`{val}` is {get_bool_from_any(val)}")
Changing data types
To understand what's going on in that function, we need to consider data types. Python is not a strongly-typed language like C or Java. However, the language does define data types, and generally requires you to operate on one type at a time. For example, you can't add an integer and a list.
When data types don't match, the Python interpreter will do its best to convert values so they do match. However, its rules for this coercion don't always match what you might expect.
Originally, Python did not have a Boolean data type. One was added in PEP 285 (way back in 2002!). In Python, bool
inherits from int
in order to maintain compatibility with code that was written before this type was added. This is why 1 == True
evaluates to be a true statement. It also means you can add 1 + True
to get 2
. Weird.
Coercion rules in Python
That PEP and its implementation defined how the interpreter will coerce values into Booleans, which is summarized in this table.
Input value | Coerced to |
---|---|
An empty string | False |
The string "False" |
False |
Any other non-empty string | True |
The number 0 or 0.0 |
False |
Any other number, positive or negative | True |
An empty list, object, or set | False |
A list, object, or set with members | True |
For most situations, those rules are great. However, you'd probably expect bool("false")
to return False
when in fact it returns True
. Lots of situations treat -1
as a falsey value. And silly humans use strings like "Yes", "No", and so forth to represent Boolean-like values. We can do better!
Digging into get_bool_from_any()
We finally have the background we need to understand the get_bool_from_any()
function above. The try
block that starts on line 7 handles numbers, as well as strings that can be converted to numbers like "0.1". For my purposes, it made sense that any value greater than 0 would be True
and the rest False
.
The float(val)
call will raise an exception if val
can't be converted to a number. So the next thing the function handles is strings. As the comment says, I use ast.literal_eval()
rather than plain eval()
since it's less vulnerable to injection attacks. (This function comes from the ast
or Abstract Syntax Tree built-in library.) To handle upper- and lowercase variations, I convert to lowercase, then lead-capitalize the string. In this way, "false"
will be parsed and converted to False
as we'd expect.
The ast.literal_eval()
function will raise an exception in a couple of cases. If it can't parse the word — in other words, it's not "False" or "True" — I catch the ValueError
and test to see if the string is in my short list of falsey words. If so, the function returns False
otherwise it returns True
.
The other except
block takes care of empty strings and multi-word strings. On its own, ast.literal_eval()
raises a SyntaxError
in those situations. I just treat those cases as falsey values and return False
.
Finally, I handle input values that are something other than a string or number. Line 27 uses Python's bool()
function to explicitly convert the input value to a Boolean. Following the rules described in the table above, this handles cases like empty or populated lists, objects, and so forth.
Conclusion
I really didn't need a function that was this comprehensive. The callback payloads I was handling always contained strings, and the values were always one of a short list of variations. I mean, my function could have been as simple as:
def is_it_true(val):
return val in ["true", "True", "yes", "Yes", "1"]
But, where's the fun in that? With my function, I handle just about any data type as an input value. I handle human-intuitive values like -1
being a falsey value. And, I got to explore some library functions I would rarely use otherwise. Figuring out the logic of the if-else and try-except blocks gave my brain a little exercise too.
I hope you find the function useful, or you at least learned something from this post. If you find an error or an edge case, hit me up on Mastodon or put in a PR on this blog's repo; links are in the sidebar.