skip to content

Why is bool a subclass of int in Python, and where does that bite?

level: middleimportance: must knowfreq 55%

answer

  1. Booleans arrived late in the language
  2. They kept working like 1 and 0
  3. It is a real subclass, not a lookalike
  4. isinstance guards let True through
  5. True and 1 are the same dict key

basics

~20 s

bool inherits from int, with True equal to 1 and False equal to 0. So booleans do arithmetic, sum() counts them as flags, isinstance(x, int) accepts them, and True collides with 1 as a dictionary key.

solid answer

~40 s

`bool` is a genuine subclass of `int`: `issubclass(bool, int)` is True, `True == 1`, `False == 0`, and `True` and `False` are singletons. That was a compatibility decision - booleans were added late, after years of code that returned 1 and 0, so making them integers kept that code working. The useful consequence is that `sum(flags)` counts how many are true and a boolean can index a sequence. The dangerous consequences are that `isinstance(value, int)` happily accepts `True`, so a validation guard for a count or an id lets a boolean through, and that `True` hashes and compares equal to `1`, so `{1: 'a', True: 'b'}` collapses to a single entry keyed by `1`. You cannot subclass `bool` further; the type is final.

code

pycon · 10 lines
pycon
>>> issubclass(bool, int)
True
>>> True + True
2
>>> sum([True, False, True, True])
3
>>> {1: 'one', True: 'bool'}
{1: 'bool'}
>>> ('no', 'yes')[True]
'yes'

go deeper

for a junior

Recall that True is 1 and False is 0, so sum() over a list of booleans counts the true ones. Knowing that bool is literally a subclass of int is what the interviewer is listening for.

for a middle

Explain the mechanics and the history: PEP 285 added bool late and subclassed int for compatibility, True and False are singletons, and equality plus hashing make True and 1 the same dict or set key.

for a senior

Show where it produces real defects - isinstance guards accepting booleans, schema or serialization layers matching the integer rule first, and lookup tables mixing booleans with small integers - and give the explicit bool-first check as the fix.

for a principal

Own the boundary policy: decide where booleans and integers must be distinguished across serialization, storage and API contracts, and make that rule explicit once rather than leaving each caller to rediscover that a bool satisfies an int contract.

## The type relationship `bool` is a subclass of `int`. `issubclass(bool, int)` is True, `isinstance(True, int)` is True, and `True` and `False` are the only two instances - they are singletons, so `x is True` is a legitimate identity check. Numerically `True` is 1 and `False` is 0, and every integer operation is inherited, which is why `True + True` is 2 rather than a `TypeError`. The reason is historical. Python did not have a boolean type at first; comparisons returned the integers 1 and 0, and a great deal of code relied on that. When PEP 285 introduced `bool` in Python 2.3, making it a subclass of `int` meant existing code kept working unchanged. Nothing about this has changed since, including in 3.14. ## The idioms it enables Because a boolean is an integer, counting truths is arithmetic: ```pycon >>> flags = [True, False, True, True] >>> sum(flags) 3 >>> len(flags) - sum(flags) 1 ``` The same property makes a boolean usable as an index, since the value participates in the integer protocol: `('no', 'yes')[is_enabled]` is a legal, if terse, two-way lookup. It also means a boolean flows into any numeric aggregation - an average of booleans is a rate, and multiplying by a boolean is a mask. ## Where it bites **Type guards.** The single most common real bug. A function that means to accept a count writes `isinstance(times, int)`, and `True` passes: ```python def repeat(text: str, times: int) -> str: if not isinstance(times, int): raise TypeError('times must be an int') return text * times print(repeat('ab', True)) # 'ab' - a bool sailed through the guard ``` If you truly mean 'an integer and not a boolean', the check is `isinstance(value, int) and not isinstance(value, bool)`. The same holds for a schema layer that maps Python types to column types: a boolean will match an integer rule unless the boolean rule is tested first. **Dict and set keys.** Equal values that hash equally are the same key. `hash(True) == hash(1)`, so: ```pycon >>> {1: 'one', True: 'bool'} {1: 'bool'} >>> {True, 1, 1.0} {True} ``` The surviving key is the first one inserted while the value is the last one written, which produces the genuinely confusing `{1: 'bool'}`. A lookup table keyed by a mixture of small integers and booleans - a status map, say - silently loses entries. **Mixed bitwise operands.** `&` and `|` between two booleans return a `bool`, but mixing in an integer returns an `int`: `True & True` is `True`, while `True & 2` is `0` and `True | 2` is `3`. Code that treats `&` as a boolean 'and' on non-boolean operands gets an integer answer, and integer answers are truthy in surprising ways. **`and` / `or` do not return booleans at all.** They return one of their operands - `'' or 'default'` is `'default'`, `0 or []` is `[]`. That is a separate mechanism from `bool`, but it is where the same confusion shows up in reviews. ## Truthiness is a different question `bool(x)` calls the object's `__bool__` and falls back to `__len__`, so any object can be truthy or falsy without being a `bool`. `if items:` is an emptiness test, not a boolean-typed comparison, and comparing with `== True` both breaks for truthy non-boolean values and adds nothing when the value really is a boolean. ## Serialization and storage The subclassing is a Python-side fact, not a wire-format one. JSON serialization writes `true` and `false`, not `1` and `0`, because the encoder checks for `bool` before `int`. Storage layers that lack a boolean type - SQLite, for instance - persist booleans as integers and hand you back `1` and `0`, so a round trip loses the type even though it preserves the value. If a boundary must distinguish them, check for `bool` explicitly and check it first. ## One more detail `bool` is final: `class Tri(bool): pass` raises `TypeError`, because the type is intended to have exactly two instances. If you need a three-state flag, you use `None` plus a boolean, an enum, or your own class - not a boolean subclass.

  • What does the dict literal {1: 'one', True: 'bool', 1.0: 'float'} evaluate to, and why?
    `{1: 'float'}`. All three keys compare equal and hash equally, so they are one key. Insertion keeps the first key object seen - the integer `1` - while each later assignment overwrites the value, so the last value wins. This is the same rule that makes a lookup table mixing booleans and small integers silently lose entries.
  • How do you write a check that accepts an integer but rejects True and False?
    Test the boolean case explicitly and test it first: `isinstance(value, bool) or not isinstance(value, int)` means 'reject'. Type checkers do not save you here either - a `bool` is a valid `int` in the type system, so a static checker will accept passing `True` to an `int` parameter. If booleans must be rejected, it has to be a runtime check or a distinct parameter type.
  • Can you subclass bool to add a third state?
    No - `bool` is final and `class Tri(bool): pass` raises `TypeError`, because the type is defined to have exactly two singleton instances. Model three states with `None` plus a boolean, with an enum, or with your own class. Using an integer subclass to sneak in a third truth value also breaks every consumer that assumes two values.

Booleans were bolted onto a house whose wiring already ran on 1 and 0, so they were wired as a special kind of integer rather than a new circuit.

saying these in an interview costs you the question

  • Says bool is a separate type unrelated to int
  • Expects True + True to raise TypeError
  • Thinks True and 1 are distinct dict keys
  • Uses isinstance(x, int) to reject booleans
  • Compares with == True instead of testing truthiness
  • Claims bool can be subclassed for a third state

context