cswork / libraryOpen practice workspace

Articles

Python singledispatch Interview Questions: Dispatch Types and Registration

Practice Python singledispatch with first-argument traces, bool inheritance, registration changes, virtual ABC ambiguity, and handler-contract tests.

You register an int handler, call the function with True, and reach that handler. You then register bool, repeat the call, and get a different result. A Python singledispatch interview question becomes much easier when you explain those two decisions separately: the runtime type of the first argument selects an implementation, and the registry determines which implementation is available.

The main trap is treating singledispatch as automatic validation or multiple-argument overload resolution. It does neither. Build an answer around a small registry, predict each selected handler, and test a registration change. For a practical extension problem, use Design an Extensible Python File Parser and consider dispatching already-parsed domain objects.

Evidence boundary: Official semantics come from CPython's current 3.14 documentation, implementation, and PEP 443. The examples are original exercises checked on CPython 3.12.14, not candidate reports or claims about an employer's interview format. Union-annotation registration requires Python 3.11 or later. Historical PEP discussions do not override the current library API.

Which argument chooses the implementation?

functools.singledispatch creates a generic function whose implementation is selected from the type of its first positional argument. The original function supplies the object fallback. Ordinary subclass relationships can select a registered ancestor when there is no exact registration. The current library reference documents explicit registration, annotation-based registration, and inspection through dispatch() and registry. CPython functools documentation.

Use this original fixture:

from functools import singledispatch

@singledispatch
def label(value, context=None):
    return "fallback"

@label.register(int)
def integer_label(value, context=None):
    return "integer"

assert label(7, "screen") == "integer"
assert label(7, []) == "integer"
assert label("7", "screen") == "fallback"
assert label(True) == "integer"

The first two calls choose the same implementation despite different second arguments. The third has a different first-argument type. The fourth reaches int because bool is an integer subclass and there is no more specific handler yet.

Do not explain the result as Python converting True to 1. The value remains a Boolean object; the selection policy found an applicable registered type. You can prove that by logging type(value).__name__ inside the integer handler. It will still report bool for that call.

The dispatcher needs a positional argument to inspect. Calling label(value=7) with no positional argument is not the same contract as label(7); in this fixture it raises TypeError before selecting the integer handler. Test the actual public calling convention if you wrap the dispatcher in another API.

Also keep selection separate from success. A handler may be chosen correctly and then reject its input. If your integer handler divides by a supplied denominator, the second argument still affects execution and possible errors even though it does not choose the implementation.

What changes when you register bool later?

Continue the same fixture:

before = label.dispatch(bool)
assert before is integer_label

@label.register(bool)
def boolean_label(value, context=None):
    return "boolean"

assert label(True) == "boolean"
assert label.dispatch(bool) is boolean_label
assert label(7) == "integer"

An exact Boolean handler now wins for Boolean inputs, while ordinary integers continue to use the integer handler. The previous dispatch decision is not permanently frozen. CPython's implementation clears its dispatch cache when registration changes. That is a cache of handler selection, not a cache of function return values. CPython functools implementation.

A useful follow-up is to call label(7) twice while the handler increments a counter. The counter should advance twice. Calling singledispatch is not equivalent to applying an LRU cache to the computation. If memoization is needed, define that additional policy explicitly and consider whether registry changes should invalidate its results.

Our recommendation for a production plugin system is to establish registrations during a controlled initialization phase. Late registration is supported, but allowing arbitrary request paths to replace handlers makes behavior depend on timing. Record which plugin owns each registration and reject unintended replacements if the application needs that invariant.

Do annotations inspect container elements?

No. A registration key describes a runtime class, not a predicate over every contained value. This original fixture uses an explicit key so the distinction remains visible:

@label.register(list)
def list_label(value: list[int], context=None):
    return "list"

assert label([1, 2]) == "list"
assert label(["a", "b"]) == "list"
assert label([]) == "list"

The explicit registration key is list. The annotation describes an intended static contract; singledispatch does not scan every element to enforce it. Registering list[int] as though it were a distinct runtime class is the wrong model.

In an interview, ask whether string elements should be rejected, coerced, or handled by a different operation. Those are business and validation decisions. If the answer requires separate integer-list and string-list behaviors, an empty list creates another policy question: which category would it represent? Runtime element inspection has costs and ambiguity that first-argument type dispatch deliberately does not solve.

For a genuine distinct representation, consider domain wrapper classes such as NumericBatch and TextBatch, each with a validated construction path. That proposal is engineering inference, not a requirement imposed by singledispatch. It can make the dispatch key meaningful instead of hiding element-sensitive branching inside one registered list handler.

How should you use annotation and union registration?

For a plain runtime class, a handler's first parameter annotation can supply the registration type:

@label.register
def float_label(value: float, context=None):
    return "floating"

For a shared behavior on supported versions, a union annotation can register the same handler for multiple classes. Avoid claiming that every typing construct is a valid runtime dispatch category. A union of actual classes and a parameterized generic container represent different things.

A clear original exercise is to define a fresh generic function for rendering numbers, then register one handler with int | float. Predict 4, 4.5, True, and "4". After that, add a separate Boolean handler and repeat. The interesting part is the policy change, not merely demonstrating union syntax.

Keep signatures compatible. If the generic function accepts context=None, but one registered implementation omits that parameter, a valid dispatch may still raise TypeError when the caller supplies context. Write tests through the generic function as well as direct tests of the handler.

The registered handler name is not the dispatcher. Registration returns the implementation itself, allowing direct handler tests; calls through label exercise selection. A useful debugging statement names both the dispatch type and selected callable, so a failure can be attributed to selection or execution rather than described vaguely as “the decorator broke.”

Can virtual abstract base classes make dispatch ambiguous?

PEP 443 explains how registered abstract base classes participate in dispatch beyond ordinary declared inheritance. A class can be recognized as a virtual subclass of two unrelated registered ABCs. When neither is more specific, choosing whichever was registered most recently would conceal an unresolved policy. The documented design raises an ambiguity error. PEP 443.

This original fixture makes the conflict explicit:

from abc import ABC
from functools import singledispatch

class Left(ABC):
    pass

class Right(ABC):
    pass

class Both:
    pass

Left.register(Both)
Right.register(Both)

@singledispatch
def choose(value):
    return "fallback"

@choose.register(Left)
def choose_left(value):
    return "left"

@choose.register(Right)
def choose_right(value):
    return "right"

Calling choose(Both()) raises RuntimeError for ambiguous dispatch in this setup. There is no declared Left, Right class hierarchy inside Both.__mro__ that supplies the simple tie-breaker a candidate might expect.

Repair it by stating the desired policy and registering Both explicitly. For example, a handler returning "combined" removes the ambiguity for that exact class. Merely reversing the two registration statements is not a defensible business rule.

Do not generalize this fixture into “multiple inheritance always causes ambiguous dispatch.” Ordinary declared inheritance has an MRO, and a more specific applicable type can resolve a choice. The failure here deliberately uses unrelated implicit ABC memberships. A strong answer identifies those conditions instead of memorizing an exception name.

What changes for singledispatchmethod?

For methods, use singledispatchmethod when the intended dispatch argument follows the bound receiver. The library documents selection on the first non-self or non-cls argument. When combining it with classmethod and needing .register during class definition, the singledispatchmethod decorator must be outermost.

The original interview exercise is a Formatter with format(self, value, style="plain"). Register an integer variant and a string variant, then call both through one instance. Explain why the formatter instance does not choose the value handler and why changing style alone does not switch variants.

A poorly placed plain @singledispatch on an instance method can lead you to dispatch on the receiver instead of the intended value. State the callable's binding behavior before debugging the registry. If your design truly chooses behavior jointly from receiver and value types, single dispatch is an incomplete representation of that requirement.

How do you test a dispatcher without hiding its failures?

Use a small selection matrix built from the actual supported domain. For the label example, include integer, Boolean before and after registration, string fallback, empty list, and string-containing list. For the ABC example, include the ambiguous class before repair and its exact-type handler afterward.

Test the handler contract separately: arguments, return type, exceptions, and side effects. A direct integer_label(7) test can pass even if the public dispatcher selects the wrong callable. Conversely, selecting the expected callable does not prove that it handles a particular context safely.

For plugins, create a fresh dispatcher per test or initialize registrations deterministically. A shared registry modified by one test can alter another test's result. Do not fix that by relying on alphabetical test order. Document the initialization phase and make the registry's expected entries part of the test evidence.

Finally, test a class unknown to the registry. Decide whether the fallback should serialize generically or raise a clear unsupported-type error. Silent acceptance can be worse than an exception when a handler is expected to redact or validate data.

Sources and Further Reading

Sources checked October 1, 2026. Examples target Python 3.11+ features where indicated and were checked on CPython 3.12.14.