True, False, One, and Zero
Recently, I stumbled upon a bug in JinjaX. It’s one of those bugs that’s so basic you feel silly for not catching it sooner, but also so specific to Python that it’s almost forgivable. Let’s dive into the code, can you see it?
def set(name, value):
if value in (False, None):
remove_attr(name)
else:
add_attr(name, value)
The problem arises when the value is 0. You see, in Python, True
and False
are global constants with values 1 and 0, respectively. Here’s a pseudocode version to clarify:
const True = 1
const False = 0
This is why linters push you to write something is False
instead of something == False
. The is
checks if something
is the singleton False
, while ==
checks if it’s equal to zero.
0 == False # True
0 is False # False
0 in (False, None) # True
That last line is the culprit. Using "in" meant I didn’t realize (and neither did the linter) that I was comparing values.
Digging into the history, it gets even crazier. In Python 2.2 and beyond, True
and False
were global variables! You could reassign them to anything:
True = 42
False = -1
True, False = False, True # :evil:
Why after 2.2? Because before that, Python didn’t even have True
or False
. Programmers would define them at the start of their scripts:
True, False = 1, 0
When True
and False
were introduced, they had to be variables instead of reserved words to maintain backward compatibility with those old scripts until Python 3 came along and fixed this insanity.
So there you have it—a seemingly trivial bug with a fascinating backstory. Python’s evolution is a testament to the challenges of maintaining backward compatibility while pushing a language forward.
Hi I’m Juan-Pablo Scaletti
I’m a software writer and open-source creator. This is my corner of the Internet.