How singledispatch resolves types

functools · Expert · 10 min

Ambiguous registrations and surprising choices make sense once you see the resolution algorithm.

When a singledispatch function is called, it looks up type(arg) in a dispatch cache (a WeakKeyDictionary keyed by class). On a miss, it computes the best implementation:

1. It builds an MRO for the argument's class that also includes the ABCs that have registered implementations and that the class is a virtual subclass of (via register or __subclasshook__). This uses the same C3 linearisation as normal inheritance (functools._compose_mro). 2. It walks that MRO and picks the first class with a registered implementation. 3. If two ABCs match at the same level with no ordering between them, it raises RuntimeError: Ambiguous dispatch. Register an implementation for the concrete class to resolve it.

The cache is invalidated whenever you register a new implementation, and also when abc.ABCMeta's registry changes (tracked via a cache token), so registering virtual subclasses later is picked up.

Registration via annotations accepts classes and, since 3.11, X | Y unions and typing.Union; it rejects parameterised generics like list[int], because dispatch only sees runtime classes, not element types.

Dispatch is on args[0].__class__, so proxies that override __class__ dispatch as the class they claim to be. Keyword-only calls fail: there must be at least one positional argument.

Example

from collections.abc import Iterable, Sized
from functools import singledispatch

@singledispatch
def describe(obj): return "object"
@describe.register
def _(obj: Sized): return "sized"
@describe.register
def _(obj: Iterable): return "iterable"
class Box:                    # virtual subclass of Sized only
    def __len__(self): return 1
class Bag:                    # virtually both Sized and Iterable
    def __len__(self): return 0
    def __iter__(self): return iter(())
print(describe(Box()))        # sized
try:
    describe(Bag())
except RuntimeError as e:
    print(str(e)[:18])        # Ambiguous dispatch
describe.register(Bag, lambda obj: "bag")
print(describe(Bag()))        # bag
try:
    describe.register(list[int], lambda obj: "ints")
except TypeError:
    print("generics rejected")   # generics rejected

Remember

  • Dispatch walks an MRO extended with relevant ABCs, cached per class
  • Unordered matching ABCs cause 'Ambiguous dispatch'; register the concrete class
  • Only runtime classes dispatch; parameterised generics are rejected

Watch out for

  • Registering for list[int] and list[str] expecting dispatch on element types.

Check yourself

  1. Why does describe(Bag()) raise RuntimeError in the example?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.singledispatch

Builds on

Unlocks

Nothing builds on this yet.