Python “bool” — wat?

what I learned about bool while implementing it for Batavia

The bool type for Python was proposed in PEP 285 by Guido van Rossum, and adopted in Python 2.3.

    This PEP proposes the introduction of a new built-in type, bool,
with two constants, False and True. The bool type would be a
straightforward subtype (in C) of the int type, and the values
False and True would behave like 0 and 1 in most respects

Under the Review section of PEP 285, the questions of arithmetic using bools and bool inheriting from int were addressed:

   4) Should we strive to eliminate non-Boolean operations on bools
in the future, through suitable warnings, so that for example
True+1 would eventually (in Python 3000) be illegal?

=> No.

There's a small but vocal minority that would prefer to see
"textbook" bools that don't support arithmetic operations at
all, but most reviewers agree with me that bools should always
allow arithmetic operations.
   6) Should bool inherit from int?

=> Yes.

In an ideal world, bool might be better implemented as a
separate integer type that knows how to perform mixed-mode
arithmetic. However, inheriting bool from int eases the
implementation enormously (in part since all C code that calls
PyInt_Check() will continue to work -- this returns true for
subclasses of int). Also, I believe this is right in terms of
substitutability: code that requires an int can be fed a bool
and it will behave the same as 0 or 1. Code that requires a
bool may not work when it is given an int; for example, 3 & 4
is 0, but both 3 and 4 are true when considered as truth
values.

That’s Python’s BDFL writing, so that’s what happened. Most of the time, nothing unusual happens as a result of these design decisions. This blog post is about the other times.

Pop quiz

If you’d like to test your own knowledge of how bool behaves, do this 15-question quiz now, and then come back here and keep reading. (Note that the quiz specifies Python 3.5; some of the more interesting results are different in Python 3 compared with Python 2.)

Did you find the quiz easy? At the time of writing, with over 100 responses to the quiz, the average score is only 44%, and the highest score is 80%.

Distribution of quiz scores (out of 15)

Let’s look at those questions and try to understand the “wat‽” in each one.

1. True + True

Answer: 2

This result comes directly from the representation of True as 1, and bool’s inheritance from int: 1 + 1 = 2.

2. True – True

Answer: 0

Same explanation as for question 1: 1 – 1 = 0.

Note that the font that medium.com uses makes it tricky to distinguish the digit zero (“o”) from the lowercase letter “o”; in this article any “0” not part of a word is a zero.

3. True % 1

Answer: 0

The modulo operator % gives the remainder when divided by 1, which is always 0 for integers: 1 % 1 = 0.

4. True % 2

Answer: True

Our first real wat. 1 % 2 = 1, so why does Python return True instead?

Why does the type of the result change just because the value (not type) of one of the operands changes? I’ll pause that thought for a minute and pick it up after the next couple of questions.

5. False % 1

Answer: False

6. False % 2

Answer: False

At least 5 and 6 are consistent: 0 % 1 = 0 and 0 % 2 = 0, only Python is returning a bool for both, just like it did for question 4.

My guess is that there’s some kind of short-circuit evaluation going on, which returns the first operand of “a % b” without doing any type-casting if these conditions are met:

  • a >= 0 and b > 0
  • a < b

I’m not an expert on the CPython internals, so I haven’t been able to confirm this by digging around the source. (If you can confirm or deny this guess, please leave a comment.)

7. False % 1.0

Answer: 0.0

Since one of the operands is a float, False is type-cast to a float before the operation, and the result is also a float.

8. True % True

Answer: 0

This is really just question 3 again.

9. False % True

Answer: False

This is question 4 again.

The rest of the questions are more about complex numbers and division by zero exceptions, and many more people got those questions correct, so I’ll only cover them briefly.

10. False / 1j

Answer: 0j

In case you haven’t seen it before, “1j” is an imaginary literal, used to build up values of the complex type. In mathematics, j = sqrt(–1), a number that isn’t a member of the set of real numbers.

There’s no real trick to this question (pun intended); False is type-cast to complex, so the result will be of type complex.

11. True / –1j

Answer: (–0+1j)

All the possible answers to this question that were given are effectively equal, so the question was asking how Python represents complex numbers, and under what circumstances the real part of a complex number would be “–0” instead of “0” (positive zero).

  • The real and imaginary parts of complex are always of type float, so Python doesn’t bother to add “.0” to numbers that appear to be integral.
  • Python’s float implementation distinguishes 0 from –0, and the sign is negative if the operands have different signs. (Compare the case with floats: “0.0 / –1.0 = –0.0”.)

12. False / –1j

Answer: (–0–0j)

The explanation for question 11 applies here too.

13. True // False

Answer: ZeroDivisionError: integer division or modulo by zero

14. 0.0 / False

Answer: ZeroDivisionError: float division by zero

15. 0.0 // False

Answer: ZeroDivisionError: float divmod()

There’s no tricks here, just differently worded exception messages to choose from.

Conclusion

Just considering questions 1 to 9, which were the ones that dealt primarily with bool, the average score on the quiz was just 22%, and the highest score was 56%, both of which are much worse results than for the quiz overall.

From this I conclude that most people who answered the quiz don’t really understand the behaviour of Python’s bool type very well — but considering that the questions dealt mainly with obscure edge cases, it doesn’t really matter. 😛

Like what you read? Give Tim Bell a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.