skip to content

singledispatch and Generic Functions

singledispatch turns one function into a generic function with implementations registered per first-argument type. Interviewers use it to see whether you extend behavior without editing a hierarchy.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

How does functools.singledispatch pick an implementation when several registered classes match the argument?

level: middleimportance: must knowfreq 45%

answer

  1. Specificity, not the order you wrote
  2. It walks the argument class's ancestors
  3. Nearest registered ancestor wins, object last
  4. Abstract base classes join the walk
  5. Two unrelated matches raise instead of guessing

basics

~10 s

It takes the class of the first positional argument, walks that class's method resolution order, and uses the implementation registered for the nearest ancestor, falling back to object. Registration order is irrelevant; specificity decides.

solid answer

~40 s

The wrapper reads `__class__` off the first positional argument and resolves it against the registry by walking that class's method resolution order — the linearised ancestor list — and taking the first entry that has a registration. Nearest wins, so the order you wrote the registrations in never matters. That is why registering `int` also catches `True` (`bool` subclasses `int`) and registering `datetime.date` catches `datetime.datetime`. Abstract base classes participate too: the resolver composes them into the walk, so registering `collections.abc.Sequence` matches `str`, `list` and `tuple`. When two *unrelated* abstract base classes both match and neither is more specific, it refuses to guess and raises `RuntimeError: Ambiguous dispatch`; register the concrete class to break the tie. Results are cached per class, and registering a new implementation clears that cache.

code

pycon · 19 lines
pycon
>>> import functools, datetime
>>> @functools.singledispatch
... def fmt(value):
...     return "generic"
...
>>> @fmt.register
... def _(value: datetime.date):
...     return "date"
...
>>> @fmt.register
... def _(value: bool):
...     return "bool"
...
>>> fmt(datetime.datetime(2026, 1, 1))
'date'
>>> fmt(True)
'bool'
>>> fmt(1)
'generic'

go deeper

for a junior

Remember the rule of thumb: the most specific registered class wins, and the order in which you wrote the registrations does not matter. Know that an unregistered class falls through to the decorated body registered for object.

for a middle

Explain the walk over the argument class's ancestor list, why bool matches an int registration and datetime matches a date registration, and how abstract base classes are spliced into that walk so a list matches a Sequence registration.

for a senior

Show you would catch ambiguity before production: assert coverage with the dispatch attribute in tests, register concrete classes to break ties between unrelated abstract base classes, and treat the call-time RuntimeError as a latent bug rather than a rare edge case.

for a principal

Own the design consequence: resolution by specificity means a registration added anywhere can change behaviour for classes elsewhere. Decide whether registration keys are concrete classes owned by your team or abstract base classes that anyone's class may satisfy accidentally.

## The resolution walk Every call does two things: read the class of the first positional argument, then find the best implementation for that class. The first step reads `__class__` rather than calling `type()`, which matters only for proxy objects that lie about `__class__` — such an object is dispatched as the class it claims to be. The second step is the interesting one. The registry is keyed by class, and the argument's class is usually not in it — you register `collections.abc.Mapping` and the caller hands you some concrete mapping class. So the resolver walks the argument class's **method resolution order**: the linearised list of ancestors that the class itself uses for attribute lookup, starting at the class and ending at `object`. The first ancestor that has a registration wins. Because `object` is always registered (that is the decorated body), the walk always terminates with an answer. Two consequences follow immediately. **Registration order is irrelevant.** Whether you register `date` before or after `datetime` changes nothing; the ancestor list decides. This is the single biggest behavioural difference from an `if/elif isinstance` chain, where a base-class branch written first silently swallows every subclass and the subclass branch below it is unreachable. **Inheritance you forgot about still counts.** The classic traps in the standard library's own hierarchy: - `bool` is a subclass of `int`, so an implementation registered for `int` handles `True` and `False` unless you register `bool` separately. - `datetime.datetime` is a subclass of `datetime.date`, so registering `date` quietly handles datetimes — usually not what a formatter wants. - `str` is a `collections.abc.Sequence`, so registering `Sequence` to mean "a list-like thing" also captures strings, which are then iterated character by character. ```python import functools, datetime @functools.singledispatch def fmt(value): return "generic" @fmt.register def _(value: datetime.date): return "date" print(fmt(datetime.datetime(2026, 1, 1))) # 'date' ``` ## Abstract base classes and virtual subclasses A class can be an ancestor without appearing in the ordinary ancestor list: abstract base classes accept *virtual* subclasses, either registered explicitly or recognised structurally by a subclass hook. `list` is not literally derived from `collections.abc.Sequence`, yet it counts as one. The resolver handles this by composing a merged ordering that splices the relevant abstract base classes into the class's own ancestor list before walking it. So abstract base classes are fully usable as registration keys, and a concrete class registered directly still beats an abstract base class that the same object also satisfies. ## Ambiguity is an error, not a guess The merged ordering can be genuinely undecidable. If an object satisfies two abstract base classes that are unrelated to each other — neither is an ancestor of the other — and both have registrations, there is no principled "nearest". Rather than pick arbitrarily, the resolver raises `RuntimeError: Ambiguous dispatch` naming both candidates. ```python import functools from collections.abc import Sized, Container @functools.singledispatch def size(value): return "generic" @size.register(Sized) def _(value): return "sized" @size.register(Container) def _(value): return "container" size([1, 2]) # RuntimeError: Ambiguous dispatch ``` The fix is to be explicit: register the concrete class you actually care about, so the walk finds it before reaching either abstract base class. Note that this error surfaces at **call** time, on whatever class first triggers the collision — a registration that looks fine in isolation can blow up on one caller's input months later, which is a good argument for asserting coverage in tests. ## Proving what will happen Two attributes make the behaviour testable without calling anything. `name.registry` is a read-only mapping of registered class to implementation. `name.dispatch(cls)` runs exactly the resolution walk described above and hands back the function that `cls` would select. A test that asserts `fmt.dispatch(bool) is not fmt.dispatch(int)` is a direct, readable way to pin down the `bool`/`int` trap, and one that asserts `fmt.dispatch(SomeClass) is not fmt.dispatch(object)` proves a plugin module was actually imported. ## The cache Resolution results are memoised per class in a weak-keyed cache, so the walk runs once per class rather than once per call and steady-state dispatch is close to a dictionary lookup plus a call. Calling `register` invalidates that cache, because a new registration can change the answer for classes already resolved. Registration is therefore an import-time activity: mutating the registry while other callers are dispatching changes shared state underneath them for no benefit.

  • Why does an implementation registered for int also handle True?
    Because bool is a subclass of int, so int appears in bool's ancestor list. The walk finds no registration for bool, moves to int, and stops there. If booleans need different handling — printing 'yes' rather than '1' — register bool explicitly; being more specific, it is found first and the int implementation is untouched.
  • What does RuntimeError: Ambiguous dispatch mean and how do you fix it?
    The argument's class satisfies two registered abstract base classes that are unrelated to each other, so neither is nearer. The resolver refuses to guess and names both candidates. Fix it by registering the concrete class involved, which is found before either abstract base class, or by dropping one of the two abstract registrations.
  • How would you assert in a test that a class selects the implementation you expect?
    Call the generic function's dispatch attribute with the class: it runs the same resolution walk and returns the function that would be used, without invoking it. Compare it against the expected implementation, or against the object fallback to prove a registration module was imported. The registry attribute lists everything registered.

saying these in an interview costs you the question

  • Says the most recently registered implementation wins
  • Registers Sequence and is surprised that str matches
  • Thinks abstract base classes cannot be registration keys
  • Expects a subclass to fall through to object, not its base
  • Believes the resolver inspects the value rather than its class
  • Assumes an ambiguous match is silently resolved somehow

context

open as a page

What does @functools.singledispatch do to a function, and how do you register an implementation for a type?

level: juniorimportance: should knowfreq 30%

basics

~20 s

functools.singledispatch turns the decorated function into a generic function. Its original body becomes the fallback implementation registered for object, and the decorator adds a register attribute used to attach one implementation per type of the first positional argument.

open as a page

Why can't @functools.singledispatch dispatch a method's argument, and what does functools.singledispatchmethod do?

level: middleimportance: should knowfreq 28%

basics

~20 s

Inside a class body, singledispatch still dispatches on the first positional argument, which is self, so every call on one class picks the same implementation. functools.singledispatchmethod, added in 3.8, is a descriptor that dispatches on the first argument after self.

open as a page

Where does functools.singledispatch beat an if/elif isinstance chain, and where does it not?

level: seniorimportance: should knowfreq 38%

basics

~20 s

functools.singledispatch wins when the set of handled classes grows from outside the module that defines the operation: each class's owner registers its own implementation, and resolution is by specificity, not branch order. A short closed chain stays a chain.

open as a page