Python bool int Relationship: Subclass and Pitfalls
python bool int relationship: Understand how Python's bool is a subclass of int, affecting truthiness, arithmetic, comparisons, and type checks. Learn to avoid common...
The python bool int relationship is a core part of the language's type system: bool is a subclass of int. That means True and False are not just independent constants; they are instances of int with fixed values. True equals 1, and False equals 0. This is not a quirk of the interpreter; it is a deliberate design decision that simplifies the language's type hierarchy. You can verify it directly:
issubclass(bool, int) # True isinstance(True, int) # True
Because of this relationship, bool inherits all of int's methods, including arithmetic operators, bitwise operations, and comparison methods. This has practical consequences for everyday code, from how you count items to how you validate conditions.
Truthiness and Boolean Contexts
The bool type is the result of any expression that uses a comparison operator, such as ==, <, or in. It is also the type returned by bool(), which converts any object to its truth value. In a condition like if some_value:, Python implicitly calls bool() on the value. The rule is simple: False, None, numeric zero, empty sequences, and empty mappings are falsy; everything else is truthy.
Because bool is a subclass of int, bool(0) is False and bool(1) is True. But be careful: bool(2) is also True, because any non-zero integer is truthy. This can lead to confusion when you treat a non-boolean integer as a condition:
value = 2 if value == True: # This is False, because 2 == 1 is False print("not reached")
The comparison value == True is evaluated as 2 == 1, which is False. If you intended to check whether value is truthy, use if value: instead. This distinction is one of the most common sources of bugs when developers forget that True is literally the integer 1.
Arithmetic Operations and Type Conversion
Since bool inherits from int, you can perform arithmetic with True and False directly. Adding booleans works, but the result is an int, not a bool:
True + True # 2 False * 5 # 0 True - False # 1
The result type is int because arithmetic operations on bool return int; Python does not preserve the bool type through arithmetic. This is useful in some contexts, such as counting the number of True values in a list:
flags = [True, False, True, True] count = sum(flags) # 3
sum() iterates over the list and adds each element. Since True is 1 and False is 0, the total is the count of True values. Some developers find this idiom concise, but it can obscure intent. If you need a count, an explicit sum(1 for f in flags if f) is clearer, though slightly more verbose.
Arithmetic with booleans also affects code that mixes bool and int in expressions. For example, True + 2 yields 3, and False * 3 yields 0. This behavior is consistent with the subclass relationship, but it can surprise developers who expect a type error or a bool result.
Comparisons and Equality Semantics
Because True is 1 and False is 0, comparisons between booleans and integers follow normal numeric rules:
True == 1 # True False == 0 # True True < 2 # True False > -1 # True
This also means that True and 1 are equal, and False and 0 are equal. However, they are not the same object. The is operator checks identity, not equality:
True is 1 # False False is 0 # False
In practice, you rarely need is for these constants because Python caches small integers, but True and 1 are distinct objects. The equality semantics can cause subtle bugs when you use a boolean in a dictionary key or a set. For example:
d = {True: "yes", 1: "no"} len(d) # 1
Because True and 1 have the same hash and compare equal, the second assignment overrides the first. The dictionary ends up with one key. This is a direct consequence of the bool/int relationship and is worth remembering when you design data structures that mix these types.
Type Checking and isinstance()
The subclass relationship affects how you check types. isinstance(True, int) returns True, so code that validates an integer input will also accept a boolean. If your function expects an integer but receives a boolean, it may behave unexpectedly. For example:
def double(value): if not isinstance(value, int): raise TypeError("expected int") return value * 2 double(True) # returns 2
If you need to reject booleans, you must check for bool explicitly:
if isinstance(value, bool) or not isinstance(value, int): raise TypeError("expected int, not bool")
Alternatively, you can use type(value) is int to exclude subclasses, but that is often too strict because it also excludes user-defined subclasses of int. The safest approach depends on whether you want to allow subclasses. In most APIs, accepting a boolean as an integer is harmless, but in domain-specific logic it can lead to incorrect calculations. Consider whether your function should treat True as 1 or reject it.
Pitfalls and Common Mistakes
Several common mistakes stem from the bool/int relationship. One is using True and False in arithmetic when you meant to use a numeric value. Another is relying on the integer value of a boolean in a context where the intent is unclear. For example:
def is_active(user): return user.status == "active" # Later: points = is_active(user) * 10 # points is either 0 or 10
This works, but it obscures the fact that is_active returns a boolean. A reviewer might not immediately see that points depends on a boolean value. A clearer version would use an explicit conditional:
points = 10 if is_active(user) else 0
Another mistake is using True and False as indices. Since False is 0 and True is 1, you can index a list with a boolean:
choice = [ "no", "yes" ][is_ok]
This is clever but fragile. If is_ok is not a boolean but an integer, the behavior changes. It is better to use an explicit conditional.
Also, be cautious with sum() on a list of booleans when you need a count. It works, but if the list contains non-boolean values, the result may be surprising. For instance, sum([True, 2]) returns 3, which is likely not what you intended.
When to Use bool vs int in APIs and Data Models
The choice between bool and int in your own code should reflect the semantics of the value. If a value has only two states, use bool. If it has more than two, or if arithmetic operations are meaningful, use int. For example, a flag like is_verified should be bool, while a counter like retry_count should be int. Mixing them can lead to confusion because True and 1 are interchangeable in many contexts, but not all.
When designing a function that accepts a numeric parameter, decide whether booleans are valid input. If your function performs arithmetic that only makes sense for true integers, you may want to reject booleans. If you are building a configuration system where True and 1 are equivalent, you can allow them, but document the behavior.
In data models, storing a boolean as an integer in a database is sometimes necessary, but you should convert it explicitly at the boundary. For example, when reading from a database, map 0 to False and 1 to True rather than relying on the implicit conversion. This makes the code more readable and avoids surprises if the database returns other integers.
Performance and Memory Considerations
The bool type is a singleton: True and False are the only two instances, and they are pre-allocated. This means that using a boolean does not create new objects, unlike an integer that may be allocated on the fly. However, since bool is a subclass of int, operations on booleans go through the same integer machinery. The performance difference between using bool and int for simple flags is negligible in most applications. The real cost comes from the semantics: if you use an integer where a boolean is intended, you may need extra checks to validate the value, which adds overhead.
Memory usage is also minimal because booleans are singletons. An integer like 1 is also cached, but larger integers are not. If you have a large list of boolean values, using bool is more memory-efficient than using int for values like 1 and 0 because the boolean references the same two objects, while integers may create new objects for values outside the cache. This is a minor optimization, but it can matter in memory-constrained environments.
In terms of performance, avoid relying on arithmetic with booleans for critical paths. The overhead is tiny, but the code becomes harder to read. If you need to count true values, a generator expression with a conditional is often clearer and just as fast:
sum(1 for x in flags if x)
This avoids the implicit conversion and makes the intent explicit.