Why does a parameter annotated int accept a bool argument like True?
answer
- Look at the class hierarchy
- Booleans predate nothing; ints came first
- isinstance(True, int)
- bool.__mro__ has three entries
- True and 1 share a dict key
basics
~20 sBecause bool is a genuine subclass of int in Python: True is 1, False is 0, and isinstance(True, int) is True. Ordinary subtype rules therefore make every bool a valid int, and no type checker complains.
solid answer
~40 s`bool` is not a separate primitive — it inherits from `int`, so `bool.__mro__` is `(bool, int, object)`, `True + True` is `2`, and `True` and `1` are even the same dictionary key. Static checkers use normal nominal subtyping here, so a `bool` is accepted anywhere an `int` is annotated, with no special rule involved. That is convenient for counting (`sum(flag for flag in flags)`) but it also means a flag can silently slip into a function that wanted an identifier or a count, and neither the checker nor the runtime will object. The type system offers no clean way to say "int but not bool": if you must exclude it, check `type(value) is int` at the boundary, or reject `bool` explicitly and say so in the error message.
code
python · 5 linesprint(bool.__mro__)
print(isinstance(True, int), True + True, True == 1)
d = {1: "one"}
d[True] = "flag"
print(d)go deeper
Be ready to state the relationship in one sentence and back it with a fact you can type from memory, such as isinstance(True, int) being True or True + True being 2. That alone answers the question.
Explain that this is ordinary nominal subtyping rather than a typing special case, and contrast it with the int-where-float rule, which is a written-in exception because int does not inherit from float.
Show where it bites in production: booleans as dictionary keys or sequence indices, config values that arrive as flags, and the fact that the annotation int cannot exclude bool, so any guard has to be a runtime type identity check.
Own the API-design angle: decide whether your public functions should reject boolean-shaped inputs at the boundary at all, and weigh the cost of overloads and runtime guards against simply documenting that int means int or bool.
## The runtime fact underneath the typing rule Python had no boolean type until 2.3. When PEP 285 introduced one, it deliberately made `bool` a **subclass of `int`** so that all the existing code that treated truth values as `1` and `0` kept working. That decision is still visible everywhere: ```pycon >>> bool.__mro__ (<class 'bool'>, <class 'int'>, <class 'object'>) >>> True + True 2 >>> True == 1, hash(True) == hash(1) (True, True) >>> [10, 20, 30][True] 20 ``` `True` and `False` are singleton instances of `bool`, and `bool` adds almost nothing to `int` beyond a different `__repr__` and the logical operators `__and__`/`__or__`/`__xor__` returning booleans. Every integer operation works on them because they *are* integers. ## Why the checker accepts it Static type checkers for Python use **nominal subtyping** for classes: if `B` inherits from `A`, then a `B` is acceptable wherever an `A` is annotated. Because `bool` really does inherit from `int`, `def gain(channel: int)` accepts `True` under the ordinary rule — no special case is needed. This is worth separating from the neighbouring rule that lets an `int` be passed where `float` is annotated: that one is a *special case written into PEP 484* because `int` is **not** a subclass of `float`. Here the relationship is real inheritance. Chaining the two, a `bool` is also acceptable where `float` or `complex` is annotated. ## The hazards The convenience has sharp edges, and they are what an interviewer is probing for. **Dictionary and set keys collide.** `1`, `1.0` and `True` all hash equal, so `{1: "one", True: "flag"}` is a one-entry dict. The stored *key object* is whichever was inserted first, so the display can be confusing: ```pycon >>> d = {1: "one"} >>> d[True] = "flag" >>> d {1: 'flag'} ``` **Flags become numbers.** A function annotated `def read(channel: int)` will happily take `True` and read channel 1. If the argument came from a config parser that returned a boolean by accident, the mistake surfaces as wrong data rather than as a `TypeError`. **Sequence indexing.** `row[True]` is `row[1]`, which is why an accidental boolean index quietly returns the second element instead of raising. **Arithmetic on booleans is idiomatic, not a bug.** `sum(1 for x in xs if pred(x))` and `sum(pred(x) for x in xs)` are equivalent, and the second is common Python. Counting how many readings in a window exceeded a threshold with `sum(r > limit for r in window)` is exactly the intended use of the subclass. Do not present the relationship as purely a defect. ## Where the subclass stops being invisible Arithmetic hides the difference; serialization does not. `json.dumps({"ok": True})` writes `{"ok": true}` while `json.dumps({"ok": 1})` writes `{"ok": 1}`, so a boolean that travelled through an `int`-annotated parameter changes the wire format every downstream consumer sees, without any error along the way. The key collision surfaces there too: `json.dumps({True: "x"})` emits the key as the string `"true"`. The same applies to database drivers, message schemas and any `repr`-based logging: a `bool` renders as `True`/`true`, an `int` as `1`. If a value crosses a system boundary, the difference between the two types is observable even though the type checker treats one as a perfectly good instance of the other. ## Can you exclude bool? Not cleanly, and that is the honest senior answer. The annotation `int` means "int or any subclass", and `bool` is a subclass. Options, roughly in order of how often they are worth it: * **Do nothing.** In most code a stray boolean is a caller bug that shows up immediately. * **Guard at the boundary** with `if type(value) is not int: raise TypeError(...)`. Using `type(x) is int` rather than `isinstance` is the one place where the identity check is the correct tool, because `isinstance(True, int)` is `True` by design. * **Overloads.** A library can declare an overload taking `bool` and mark it as unsupported so the checker flags the call site, at the cost of a fair amount of ceremony. The symmetric question — whether an `int` may be passed where `bool` is annotated — has the opposite answer: `int` is the *parent*, so a checker rejects it. Passing `1` to a parameter annotated `bool` is an error even though `if 1:` is perfectly truthy at runtime, because truthiness and typing are different systems. ## What to say in an interview State the inheritance relationship first, back it with one runtime fact (`isinstance(True, int)` or `True + True == 2`), give the historical reason (backward compatibility when `bool` was introduced), and then show judgement by naming the dictionary-key or indexing surprise and the `type(x) is int` escape hatch. That is a complete answer in under a minute.
- Does the relationship work the other way — can you pass 1 to a parameter annotated bool?No. `int` is the parent class, so a checker rejects `1` where `bool` is annotated; substitutability runs from subclass to superclass only. At runtime `if 1:` is of course truthy, but truthiness is unrelated to the declared type, and a function that stores the value or reports it back will now hold an `int` where its own annotation promised a `bool`.
- How would you actually forbid a bool in a function that wants a plain integer?The annotation cannot express it, so enforce it at runtime with an identity check on the class: `if type(value) is not int: raise TypeError(...)`. This is one of the rare correct uses of `type(x) is ...` instead of `isinstance`, precisely because `isinstance(True, int)` is `True`. In library code you can additionally declare an overload accepting `bool` and mark it unsupported so call sites are flagged statically.
- Why does {1: 'a', True: 'b'} end up with a single entry?Dictionaries key on hash plus equality, and `hash(True) == hash(1)` with `True == 1`, so the second write updates the existing entry rather than adding one. The key object kept is the one inserted first, so the dict displays as `{1: 'b'}`. The same collision includes `1.0`, which is why mixed numeric keys are a known source of confusion.
A boolean in Python is not a different currency, it is a coin worth exactly one or zero of the same currency — so anything that accepts money accepts it without conversion.
saying these in an interview costs you the question
- Claiming bool and int are unrelated types with a coercion rule
- Saying int is a subclass of bool
- Believing a type checker flags True passed as an int
- Using isinstance(x, int) to reject booleans
- Thinking True and 1 are distinct dictionary keys
- Assuming an int may be passed where bool is annotated