skip to content

What does functools.singledispatch give you that a dict keyed on type(obj) does not?

level: middleimportance: nice to knowfreq 26%

answer

  1. One table matches exactly, one walks upward
  2. What happens to a subclass instance
  3. Method resolution order and abstract base classes
  4. Only the first argument's type decides
  5. `functools.singledispatch` versus `handlers[type(obj)]`

basics

~20 s

functools.singledispatch resolves through the argument's method resolution order and registered abstract base classes, so subclasses and virtual subclasses find a handler. A dict keyed on type(obj) matches the exact class only and misses every subclass.

solid answer

~50 s

A table keyed on `type(obj)` does an exact identity match: register a handler for a base class and an instance of a subclass looks up its own type, misses, and falls through. `functools.singledispatch` turns a function into a generic function whose implementation is chosen by walking the first argument's method resolution order, so a subclass inherits the base class's handler, and registrations against abstract base classes match virtual subclasses too. The function you decorate becomes the fallback for anything unregistered. Implementations are attached with the decorator's `register` attribute, either from a type annotation on the new function or by passing the class explicitly, and resolution is cached. The limits matter as much: it dispatches on the **first argument only**, on its type rather than its value, and cannot dispatch on a parameterized generic — `list[int]` and `list[str]` are the same to it. For a method, `functools.singledispatchmethod` skips `self` and dispatches on the next argument.

code

python · 22 lines
python
from functools import singledispatch

@singledispatch
def describe(value):
    return f"unhandled: {type(value).__name__}"

@describe.register
def _(value: int):
    return f"int {value}"

@describe.register
def _(value: list):
    return f"list of {len(value)}"

class Count(int):
    pass

print(describe(3))
print(describe([1, 2]))
print(describe(Count(9)))   # subclass resolves to the int implementation
print(describe(True))       # bool is a subclass of int
print(describe(3.5))        # falls back to the default

go deeper

for a junior

Know that functools.singledispatch exists and picks an implementation from the first argument's type, and that a dict keyed on type(obj) matches only that exact class. Recognising the decorator in code is enough at this level.

for a middle

Explain the resolution: the argument type's method resolution order is walked, abstract base classes and virtual subclasses match, results are cached, and the decorated function is the fallback. Name the limits — first argument only, type not value, no parameterized generics.

for a senior

Show the operational edges: bool landing on the int implementation, registrations that never load because their module is never imported, and the choice between a generic function and simply giving the classes a method when the classes are yours to change.

for a principal

Judge whether type-based dispatch belongs in the design at all. A generic function centralises a cross-cutting concern over classes you cannot modify; when the types are yours it competes with an ordinary method, and when the key is data it competes with a table that is far easier to inspect and validate.

## Exact match versus the type hierarchy Both mechanisms answer "which function handles this object?", but they answer it with different lookups. A hand-rolled `{SomeClass: handler}` table is looked up with `handlers[type(obj)]`, and `type(obj)` is the object's exact class. There is no walking involved. Register a handler for a base record class and pass a subclass instance and you get a miss, even though the subclass is by every other definition one of those. You can paper over it by iterating the argument's class hierarchy yourself and taking the first hit, at which point you have written a worse version of what `functools.singledispatch` already does. `functools.singledispatch` decorates a plain function and turns it into a *generic function*. The body you decorated becomes the default implementation, used when nothing more specific matches. Additional implementations are attached with the `register` attribute the decorator adds to the function; the target class can come from the new implementation's first-parameter annotation or be passed to `register` explicitly. At call time it looks at `type(first_argument)` and resolves along the method resolution order, choosing the most specific registered class, and the result is cached so repeated calls on the same type are a dict hit. ## Abstract base classes and virtual subclasses The hierarchy walk is not limited to real inheritance. Registering against an abstract base class — one of the container or number protocols, for instance — matches any class that the ABC recognises, including classes registered as virtual subclasses that share no actual base. That is what makes a generic function usable for "anything iterable" or "anything sequence-shaped" without listing concrete types, and it is not something an exact-type dict can express at all. When two registered classes both match through different paths, the resolution follows the argument type's method resolution order, and a genuinely ambiguous multiple-inheritance case raises rather than picking silently. ## The limits, which are the interesting half **Single dispatch means single.** Only the first argument's type participates. Anything that needs to branch on the second argument, or on a combination, is outside what this tool does — that is what "single" names. **Type, not value.** It cannot distinguish an empty list from a full one, or `0` from `1`. Value-based branching stays in the function body or in a plain key-based dispatch table. **No parameterized generics.** Registration takes a class. `list[int]` and `list[str]` are both `list` at dispatch time; the erasure is total, and registering a parameterized annotation is an error rather than a silently useless registration. **Subclass surprises.** Because resolution follows the hierarchy, `bool` resolves to an `int` implementation unless `bool` is registered separately — a genuine bug source when the two should behave differently. The same logic means a string will match a registration for a general sequence protocol, which is almost never what was intended. **Methods need the sibling decorator.** On a method the first argument is `self`, so plain `singledispatch` would dispatch on the class that owns the method. `functools.singledispatchmethod` exists for exactly this: it skips the instance and dispatches on the following argument. ## When to use which Use a plain dict when the key is *data* — a record kind read from a message, a command name from a CLI, a status string from an API. The key is a string or an enum member, there is no hierarchy to walk, and the table is a piece of configuration you can print, iterate and test. This is the far more common case, and reaching for a generic function here adds machinery for nothing. Use `functools.singledispatch` when the key is a *type* and the types form a hierarchy you did not necessarily author: rendering, serializing, or measuring values of many classes, including third-party ones. Its real advantage over adding a method to each class is that it works on classes you cannot modify, and it keeps a cross-cutting concern in one module instead of scattering an unrelated method across a dozen types. And if the types are yours and the behaviour is intrinsic to them, neither: give the classes a method and let ordinary attribute lookup do the dispatch. A generic function is the tool for when that option is closed to you. ## Registration hygiene Because implementations are attached wherever `register` is called, they only exist once that module has been imported. A generic function whose implementations live in modules nobody imports silently falls back to the default. Keep registrations next to the generic function, or import the registering modules explicitly at package initialization, so the set of implementations is deterministic rather than a function of import order.

  • Why does a generic function built with functools.singledispatch send a bool to the int implementation?
    Because `bool` is a subclass of `int`, and dispatch follows the argument type's method resolution order to the most specific registered class. With only `int` registered, `bool` resolves there. If booleans need different behaviour, register `bool` explicitly — its own registration is more specific and wins. The same pattern bites with strings matching a registration for a general sequence protocol.
  • How do you dispatch on the type of a method's argument rather than on self?
    Use `functools.singledispatchmethod`, added in 3.8. Plain `singledispatch` inside a class would dispatch on the first argument, which is the instance, so every call would resolve to the same implementation. `singledispatchmethod` skips `self` and dispatches on the next argument, with implementations registered the same way. It composes with `classmethod` and `staticmethod` when they are applied in the right order.
  • When is a plain string-keyed dict the better choice over a generic function?
    When the dispatch key is data rather than a type — a record kind from a message, a subcommand name, a status code. There is no hierarchy to walk, so the extra resolution buys nothing, and the table stays inspectable: you can print the supported keys, iterate them in a test, and validate them against an enum at startup. Generic functions earn their keep only when the key is a type in a hierarchy.

saying these in an interview costs you the question

  • Thinks a dict keyed on type(obj) matches subclasses
  • Believes functools.singledispatch can dispatch on two arguments
  • Expects list[int] and list[str] to register separately
  • Uses singledispatch inside a class and dispatches on self
  • Assumes registrations exist without the defining module being imported
  • Reaches for a generic function when the key is a plain string

context