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.

Comments? Write me a quick email.
Written on
Juan-Pablo Scaletti

Hi I’m Juan-Pablo Scaletti

I’m a software writer and open-source creator. This is my corner of the Internet.