The Surprising Behavior of Boolean Indexing in Python: Using Booleans as an index?

The Surprising Behavior of Boolean Indexing in Python: Using Booleans as an index?

Python is a language full of interesting quirks, and one of them is how it handles boolean values in indexing. Consider this snippet:

my_list = [0, 1]
print(my_list[True])  # Output: 1

Why does my_list[True] return 1? To understand this, we need to dive into how Python treats booleans in different contexts.

Booleans as Integers

In Python, the boolean constants True and False are actually instances of the bool class, which is a subclass of int. True has a value of 1, and False has a value of 0.

You can verify this with the isinstance() function:

print(isinstance(True, int))   # Output: True
print(isinstance(False, int))  # Output: True

This means that in many contexts, True behaves like 1 and False behaves like 0.

Booleans in Indexing

When you use a boolean as an index, Python uses its integer value. So my_list[True] is equivalent to my_list[1], which returns the second element of the list (remember, indexing starts at 0).

Similarly, my_list[False] would be equivalent to my_list[0], returning the first element.

Potential Issues

While this behavior can occasionally be useful, it can also lead to confusing and hard-to-debug code if used unintentionally.

Consider this example:

def get_item(my_list, index):
    return my_list[index]

items = [0, 1]
result = get_item(items, items.index(1))
print(result)  # Output: 1

Here, we expect get_item(items, items.index(1)) to return 1, because items.index(1) returns 1, the index of 1 in items.

But what if we accidentally pass a boolean to get_item?

result = get_item(items, True)
print(result)  # Output: 1

We might expect this to raise an error, but instead it quietly returns 1, because True is treated as the index 1. This can lead to confusing bugs.

Preventing Unintended Boolean Indexing

To prevent issues caused by unintended boolean indexing, you can explicitly check the type of the index before using it:

def get_item(my_list, index):
    if not isinstance(index, int):
        raise TypeError("Index must be an integer.")
    return my_list[index]

Now, if we try get_item(items, True), it will raise a TypeError with a clear message.

Alternatively, you could convert the index to an integer explicitly:

def get_item(my_list, index):
    return my_list[int(index)]

This will convert True to 1 and False to 0 intentionally, which might be desirable in some cases.

In general, it's best to be explicit about types and avoid relying on implicit conversions like this. By being clear and intentional in your code, you can avoid surprising behaviors and make your intentions more obvious to other developers (and to your future self!).