What does the @functools.singledispatch decorator do to the function it decorates?
answer
- One name, several implementations
- The decorated body is the fallback
- Attach implementations with register()
- Only the first positional argument decides
- Annotation or explicit type on register
basics
~20 sIt turns the function into a generic function: the decorated body becomes the fallback implementation, and its register() attribute lets you attach one implementation per argument type. Each call picks an implementation from the type of the first positional argument.
solid answer
~40 s`functools.singledispatch` converts a plain function into a **generic function** — one name with several implementations. The body you decorate becomes the implementation registered for `object`, i.e. the fallback. You add type-specific implementations through the wrapper's `register()` attribute, either by annotating the first parameter (`@fn.register` with `def _(value: int)`, supported since 3.7) or by passing the type explicitly (`@fn.register(list)`). At call time the wrapper looks at `type()` of the **first positional argument** only and calls the implementation registered for the most derived matching class; keyword arguments never take part in dispatch. The result is open extension without an `isinstance` chain: a new type gets a new registration instead of an edit to the original function.
code
python · 17 linesfrom functools import singledispatch
@singledispatch
def describe(value):
return f"object: {value!r}"
@describe.register
def _(value: int):
return f"int: {value}"
@describe.register(list)
def _(value):
return f"list of {len(value)}"
print(describe(3))
print(describe([1, 2]))
print(describe(object()))go deeper
Be ready to say in one sentence that it turns one function into several implementations chosen by the first argument's type, and to show both register forms: an annotated parameter and an explicit class.
Explain the mechanics: the decorated body is the object entry, register returns the undecorated function, dispatch reads only the first positional argument, and keyword-only calls raise TypeError.
Show how you debug a wrong dispatch in a running service — inspect the registry, use dispatch() on the class, and check that the module holding the registration was actually imported.
Own the call on whether one-name-many-implementations is the right shape for a codebase at all, versus polymorphic methods, and what it costs in traceability when registrations are scattered across packages.
A **generic function** in Python's standard-library sense is a single callable name backed by several implementations, one chosen per call from the type of an argument. `functools.singledispatch` (added in 3.4) is the standard library's way of building one, and "single" is literal: it dispatches on exactly one argument, the first positional one. ## What the decorator actually returns `@singledispatch` does not return your function. It returns a wrapper function that carries a small registry, and it enters your original function into that registry under `object`. Because every class derives from `object`, your original body becomes the **fallback** — the implementation used whenever no more specific one matches. Metadata such as `__name__` and `__doc__` is copied onto the wrapper, the same way `functools.wraps` copies it, so the generic function still looks like the function you wrote. The wrapper exposes three useful attributes: * `register()` — add an implementation for a type. * `dispatch(cls)` — return the implementation that *would* be used for that class, without calling it. Excellent in tests. * `registry` — a read-only mapping of type to implementation, which always contains at least the `object` entry. ## The three registration forms ```python from functools import singledispatch @singledispatch def describe(value): # the object fallback return f"object: {value!r}" @describe.register # by annotation (3.7+) def _(value: int): return f"int: {value}" @describe.register(list) # by explicit type def _(value): return f"list of {len(value)}" describe.register(float, lambda v: f"float: {v:.2f}") # call form ``` The annotation form reads the annotation of the **first parameter**; anything else on the signature is ignored for registration. Since 3.11 a union annotation such as `def _(v: int | float)` registers the implementation for both members. The explicit-type form is what you need when there is no annotation to read — a lambda, or a callable you did not write. Note the third, non-decorator call form: `register(type, function)` — handy for registering something defined elsewhere. Every registration returns the *undecorated* function, which is why the idiom of naming each implementation `_` works: the name is rebound each time and nothing depends on it. Give them real names if you would rather read a traceback that says `describe_int`. ## What happens on a call Dispatch is per call and cheap. The wrapper takes `type(args[0])`, looks it up in a cache, and on a miss walks the argument type's method resolution order (plus any abstract base classes registered as virtual parents) to find the most derived registered class. The chosen implementation is then called with *all* the original arguments unchanged — dispatch selects, it does not rewrite the call. Two consequences trip people up early: * **Keyword arguments never dispatch.** Calling `describe(value=3)` does not select the `int` implementation; it raises `TypeError: describe requires at least 1 positional argument`, because there is no first positional argument to look at. * **Only the first argument matters.** `def merge(a, b)` dispatches on `a` alone. Multiple dispatch on `(a, b)` is not something `singledispatch` does; you would need a nested dispatch or a third-party library. The cache is invalidated whenever the registry changes, so registering a new implementation later — from another module, at import time — takes effect immediately, including for types that had already been dispatched. ## Why reach for it The alternative shape is a chain of `isinstance` tests inside one function. That chain is *closed*: every new type means editing the function, and the branch order silently decides which test wins. `singledispatch` is *open*: a plugin module can register a handler for its own type without touching the original code, and resolution follows class relationships rather than the order you happened to write the branches in. The cost is that the dispatch decision is now spread across the codebase, so `dispatch()` and `registry` matter for debugging.
- What happens if you call a singledispatch function with only keyword arguments?It raises `TypeError: <name> requires at least 1 positional argument`. Dispatch reads `type()` of the first positional argument, so there is nothing to key on. This is a common bug when a call site is refactored to keyword-only style and the generic function silently stops being callable.
- How do you register an implementation for a type when the function has no annotation, such as a lambda?Pass the type to register explicitly: `fn.register(float, lambda v: ...)`, or use `@fn.register(float)` as a decorator. The annotation form only works when the first parameter carries a resolvable type annotation; without one, registration raises a `TypeError` telling you to supply the class.
- How would you assert in a test that a given type resolves to the implementation you expect?Call the generic function's `dispatch()` attribute with the class — it returns the implementation that would be selected, without invoking it. Compare it against the function you registered. That is far more precise than asserting on output, which can coincidentally match the fallback.
Like a hotel switchboard: callers dial one number, and the operator routes each call by who is on the line, while the default desk answers anyone unrecognised.
saying these in an interview costs you the question
- Thinks it dispatches on all argument types together
- Believes keyword arguments participate in dispatch
- Says the decorated body is never called
- Expects static type-checker overload declarations to dispatch at runtime
- Claims a new implementation requires editing the original function
- Thinks register() replaces rather than adds an implementation