What does @functools.singledispatch do to a function, and how do you register an implementation for a type?
answer
- One public name, many implementations
- The decorated body is the fallback
- A registry keyed by class
- Register by class or by annotation
- Only the first positional argument counts
basics
~20 sfunctools.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.
solid answer
~40 s`@functools.singledispatch` converts a plain function into a *generic function*: one public callable backed by a registry of implementations keyed by class. The body you decorated is registered under `object`, so it is the fallback for anything unmatched. The decorator attaches a `register` attribute with three usable forms: `@name.register(int)` naming the class explicitly, a bare `@name.register` that reads the class off the first annotated parameter (3.7+), and the plain call `name.register(int, impl)`. Since 3.11 the annotation may be a union such as `int | float`, registering the implementation for each member. Dispatch looks only at the class of the first *positional* argument; the rest of the signature is ignored, and calling with that argument passed by keyword raises `TypeError`. `name.registry` shows the mapping, and `name.dispatch(cls)` returns the implementation a given class would select.
code
python · 15 linesimport functools
@functools.singledispatch
def describe(value):
return f"object: {value!r}"
@describe.register
def _(value: int):
return f"int: {value}"
@describe.register(str)
def _(value):
return f"str: {value}"
print(describe(3), describe("a"), describe(1.5), sep=" | ")go deeper
Be ready to say what the decorator produces: one function name, a registry of implementations, and the decorated body kept as the object fallback. Know both registration forms and that only the first positional argument is examined.
Explain the mechanics: register accepts a class or reads one from the first annotated parameter, the annotation is resolved at decoration time, unions work from 3.11, and the registry and dispatch attributes let you prove a registration landed.
An interviewer expects you to place it: an operation extended by modules that the operation's author does not control, registered at import time so nothing mutates the registry under live traffic, with dispatch used in tests to assert coverage.
Own the API question — whether an operation should be a generic function at all, or a method on types you control. Generic functions buy extension without editing a hierarchy and cost discoverability, since the implementations live wherever their registering module does.
## The shape of a generic function A *generic function* in the `functools` sense is a single public name whose behaviour is chosen from the class of one argument, with the implementations stored in a registry rather than written as branches in one body. `@functools.singledispatch` (in the standard library since 3.4) builds one for you. It returns a wrapper that carries the registry plus a small API, and it registers the function you decorated under `object` — so the decorated body is not dead code, it is the fallback that runs whenever nothing more specific matches. ```python import functools @functools.singledispatch def describe(value): return f"object: {value!r}" ``` At this point `describe` behaves exactly like the function you wrote. What changed is that it now has a `register` attribute. ## Three ways to register **By class.** `@describe.register(str)` names the class explicitly and ignores annotations entirely. Use it when the parameter has no annotation, or when the annotation is not the class you want to dispatch on. **By annotation.** Since 3.7 a bare `@describe.register` reads the class from the first annotated parameter: ```python @describe.register def _(value: int): return f"int: {value}" ``` The annotation is resolved when the decorator runs, not when the function is called, so the class has to exist at that moment; a forward reference to a class defined later fails at registration with a `TypeError`. Since 3.11 the annotation may be a union — `def _(value: int | float)` registers one implementation for both members. Before 3.11 that raised `TypeError`. **As a plain call.** `describe.register(str, my_impl)` registers an existing function without decorator syntax, which is what you want when the implementation is defined elsewhere or built dynamically. The common idiom names every implementation `_`, because `register` returns the function unchanged and the name is never used again — dispatch reaches the implementation through the registry, not through its name. Giving each one a real name is equally valid and makes tracebacks and direct unit tests nicer. ## What is dispatched on, and what is not Only the **first positional argument** participates, and the wrapper reads its `__class__`. Everything else about the call — the second argument, keyword arguments, the return annotation — is irrelevant to the choice. Two consequences bite in practice. First, if there is no positional argument at all the wrapper raises `TypeError: <name> requires at least 1 positional argument`. A caller that writes `describe(value=3)` gets that error, not the `int` implementation. If your API must accept the dispatched value by keyword, either keep the first parameter positional-only in spirit or resolve the implementation yourself with `name.dispatch(...)`. Second, the registry key must be a **class**. A subscripted generic is not one: registering an implementation annotated `list[int]` raises `TypeError: Invalid annotation for 'value'. list[int] is not a class.` Register `list` and inspect the elements inside the body if you need element-level behaviour. ## Introspection The wrapper exposes two useful attributes. `name.registry` is a read-only mapping from registered class to implementation, always containing at least `object`. `name.dispatch(cls)` returns the implementation that `cls` would select, without calling it — the direct way to unit-test that a registration landed where you expected, and the tool for resolving an implementation once and calling it in a loop. There is also an internal per-class cache so repeated calls with the same class skip the resolution walk. Registering a new implementation clears that cache, which is one reason registration belongs at import time rather than in the middle of a request. ## Where it earns its place The motivating case is an operation you want to extend for classes you do not own or do not want to touch: serialising builtins and third-party classes, formatting values for a report, converting between representations. Each type's owner registers its own implementation from its own module, and the operation's original module never grows another branch. That is also the limit of the tool: when the behaviour genuinely belongs to the data, an ordinary method on the class is more discoverable and does not need a registry at all. ## The wrapper is still an ordinary function Nothing about the result is exotic. The decorator copies the original function's metadata onto the wrapper, so the name and docstring survive and the generic function documents and introspects like the function you wrote. It can be assigned, passed as a callback, imported by other modules and called normally; the registry is simply extra state hanging off it. That matters for a practical reason: swapping an existing plain function for a generic one is a non-breaking change for every caller, because the call signature and the behaviour for already-handled inputs are unchanged. You add the decorator, then add registrations one class at a time, and the fallback keeps the old behaviour for everything you have not covered yet.
- What happens if a caller passes the dispatched value as a keyword argument?The wrapper reads the class off the first positional argument, so with no positional argument at all it raises TypeError saying the function requires at least one positional argument. Keywords never participate in dispatch. If callers insist on a keyword, resolve the implementation yourself with the generic function's dispatch attribute and call it directly.
- Can you register an implementation for a subscripted generic such as list[int]?No. Registration keys must be classes, and a subscripted generic is not one — it raises TypeError at decoration time saying list[int] is not a class. Since 3.11 a union of classes is accepted, but nothing parameterised is. Register list and inspect the elements inside the implementation if element type matters.
- Does registering by annotation still work under 3.14's deferred annotations?Yes. Registration resolves the first annotated parameter's annotation eagerly when the decorator runs, so PEP 649's lazy evaluation is forced at that point. The practical consequence is unchanged: the class named in the annotation must already exist when the module is imported, otherwise registration raises TypeError rather than failing later at call time.
It is a mail room rather than a switchboard operator: instead of one person running down a list of questions about each parcel, every department has posted its own label at the counter, and the parcel goes to whichever posted label fits it most closely.
saying these in an interview costs you the question
- Says it dispatches on the types of all arguments
- Thinks the decorated body becomes dead code once implementations exist
- Believes registration order decides which implementation wins
- Registers list[int] and expects lists of ints to match
- Assumes keyword arguments take part in dispatch