Why does a functools.singledispatch handler registered for int also receive True?
answer
- Registration is not keyed by exact type
- Resolution walks the class hierarchy
- Some builtin types inherit from others
- bool subclasses int; str is a Sequence
- Unmatched types land on the object fallback
basics
~10 sBecause bool is a real subclass of int. Resolution walks the argument's class hierarchy and picks the most derived registered class, so booleans land on the int implementation unless you register bool explicitly.
solid answer
~40 s`functools.singledispatch` does not compare types for equality; it resolves them. It walks the argument type's method resolution order — including abstract base classes the type is a registered virtual subclass of — and calls the implementation attached to the most derived class it finds. `bool` inherits from `int`, so `True` reaches an `int` handler; `str`, `bytes`, `tuple` and `list` are all `collections.abc.Sequence`, so a Sequence handler swallows strings and a recursive one can blow the stack. Anything matching nothing at all lands quietly on the `object` fallback. The defences are: register the narrower type explicitly when you mean to exclude it, and make the fallback raise `TypeError` rather than guess.
code
python · 19 linesfrom functools import singledispatch
from decimal import Decimal
@singledispatch
def to_cm(value):
return float(value) # permissive fallback
@to_cm.register
def _(value: int):
return float(value)
print(to_cm(True)) # 1.0 via the int handler
print(round(to_cm(Decimal("2.675")), 2)) # 2.67, not 2.68
@to_cm.register(bool)
def _(value):
raise TypeError("boolean is not a measurement")
print(to_cm.dispatch(bool) is to_cm.dispatch(int)) # False nowgo deeper
Know that bool really is a subclass of int, so registering an int implementation quietly claims True and False as well unless you register bool yourself.
Explain that resolution walks the class hierarchy and picks the most derived registered class, and name the classic collisions: bool under int, str under the sequence abstraction.
Show the production instinct: a raising fallback rather than a plausible default, explicit narrow registrations, and dispatch assertions in tests so a silently wrong conversion cannot ship.
Set the house rule on which abstractions are allowed as registration keys, and how unhandled types should fail in data pipelines where a wrong value costs more than an outage.
Dispatch here is a *resolution*, not a lookup by exact type — and that is exactly what makes it useful and what makes it surprising. ## How the implementation is chosen Given a call, `singledispatch` takes `type(args[0])` and finds the implementation registered for the most derived class in that type's method resolution order, taking registered abstract base classes into account as well. The result is cached per type, and the cache is dropped whenever a new implementation is registered. Registration order is irrelevant; class relationships decide. The three traps that follow are all the same trap wearing different clothes. ### 1. `bool` is an `int` `bool` subclasses `int` in Python. An implementation registered for `int` therefore handles `True` and `False` too: ```python @to_cm.register def _(value: int): return float(value) to_cm(True) # 1.0 -- the int implementation ran ``` Consider a museum-catalogue importer normalising heterogeneous field values. A boolean "on display" flag arrives in the same column stream as integer dimensions, and the int implementation cheerfully turns it into `1.0` centimetres. If booleans are not valid input, register `bool` explicitly and raise from it — a registration for the narrower class always wins over the broader one. ### 2. Abstract base classes are wider than they look `str` is a registered virtual subclass of `collections.abc.Sequence`, as are `bytes`, `tuple` and `list`. A handler registered for `Sequence` that recurses into its elements will therefore recurse into a string, whose elements are one-character strings, which are also Sequences — an unbounded recursion ending in `RecursionError`. Register `str` (and usually `bytes`) explicitly before registering any Sequence handler, or key on `list`/`tuple` rather than the abstraction. The same abstraction width produces a second, rarer failure: if a class is a registered virtual subclass of two abstract base classes that both have implementations, and neither is more derived than the other, the resolution is genuinely ambiguous and `singledispatch` raises `RuntimeError: Ambiguous dispatch: <class 'A'> or <class 'B'>`. That is the library refusing to guess, and the fix is to register the concrete class directly. ### 3. The fallback catches everything, quietly The most expensive trap is the one that neither raises nor recurses. A type that matches no registration is not an error — it lands on the `object` implementation you wrote as the base. In the importer, `decimal.Decimal` is **not** a subclass of `float` and is not registered as one, so a decimal measurement slips past the float implementation and into a fallback that does `float(value)`. Everything still runs, and the catalogue quietly acquires a floating-point rounding drift: ```python round(float(Decimal("2.675")), 2) # 2.67, not 2.68 ``` Two centimetres of drift across a few thousand rows is exactly the kind of defect that is found months later by someone reconciling totals. The reason it survived is that the fallback made a plausible-looking value out of a type nobody had thought about. ## Defences * **Make the fallback raise.** `raise TypeError(f"no handler for {type(value).__name__}")` in the base implementation converts every unforeseen type from a silent guess into an immediate, named failure. Reserve a permissive fallback for cases where "do the generic thing" is genuinely correct. * **Register the narrow type when you mean to exclude it.** `bool` under `int`, `str` and `bytes` under `Sequence`. An explicit registration is the only way to say "not this one". * **Assert resolution in tests.** The generic function's `dispatch()` attribute returns the implementation that would be chosen for a class, so a test can pin `bool`, `Decimal` and `str` to the implementations you intend, without going through behaviour. * **Prefer concrete classes to abstractions** in registrations unless you have deliberately reasoned about everything the abstraction covers — which, for the sequence and number abstractions, is more than most people expect. An `isinstance` ladder has the very same subclass semantics; the difference is that the ladder makes the ordering visible in one place, while `singledispatch` resolves it by class relationship. Neither saves you from `bool` being an `int`.
- How do you stop a functools.singledispatch handler for a broad type from catching a narrower one?Register the narrower class explicitly. Resolution always prefers the most derived registered class, so a registration for `bool` beats one for `int`, and one for `str` beats one for `collections.abc.Sequence`. The narrow implementation can raise if that input is invalid.
- What does functools.singledispatch do when two registered abstract base classes both match and neither is more derived?It raises `RuntimeError` naming both classes as an ambiguous dispatch, rather than picking one. It happens with virtual subclasses registered against several abstract base classes. The fix is to register the concrete class directly so there is a most-derived match.
- Why is a permissive object fallback risky in a data-normalising function?Because an unforeseen type produces a plausible value instead of an error. A decimal value converted through `float()` acquires rounding drift that no test catches. Raising `TypeError` from the fallback turns every unhandled type into an immediate, named failure at the point it appears.
saying these in an interview costs you the question
- Thinks registration matches the exact type only
- Says booleans never reach an int handler
- Assumes registration order decides the winner
- Registers a Sequence handler without excluding str
- Treats an unmatched type as an automatic error
- Believes Decimal is a subclass of float