Multiple dispatch based on typing.overload

Looking at the original issues [1], [2], it sounds as if there was initially an idea to have @typing.overload enable actual multiple dispatch, before it became informational only. This idea seems to resurface occasionally [3].

So, why isn’t there something like @functools.multidispatch that uses @typing.overload to actually do multiple dispatch? Since libraries for multiple dispatch exist, the actual dispatching is presumably not an insurmountable problem. And with @typing.overload there is already a standard mechanism to describe the individual specialisations.

To illustrate that point, now that Python 3.11 has added the ability to return the overloads of a given function, the following is entirely possible with a bit of DIY:

from typing import Any, overload

def concat(a: list, b: list) -> list:
    return a + b

def concat(a: list, b: object) -> list:
    return a + [b]

def concat(a: object, b: list) -> list:
    return [a] + b

# decorator that does multiple dispatch with a stub function
def concat(a: Any, b: Any) -> Any:

print(concat([1], [2]))  # [1, 2]
print(concat([1], 2))    # [1, 2]
print(concat(1, [2]))    # [1, 2]
print(concat(1, 2))      # TypeError: No overload variant ...

As you can see, the idea is roughly that the overloads have all necessary type information for multiple dispatch anyway, so one only has to add the specific implementations. While this turns the usual pattern of only having stubs for @typing.overload exactly on its head, it does yield somewhat type-checked multiple dispatch right now at no extra cost.[1]

Toy implementation

Below is a very naive implementation of the dispatcher, just to run the example. It would probably be better to reuse the @singledispatch mechanism.

from functools import wraps
from inspect import signature
from typing import Any, Callable, TypeVar, cast, get_overloads, get_type_hints

F = TypeVar('F', bound=Callable[..., Any])

def _dispatch(classes, registry):
    match = next((types for types in registry.keys()
                  if all(issubclass(c, t) for c, t in zip(classes, types))),
    return registry.get(match)

def multipledispatch(func: F) -> F:
    registry = {}
    sig = signature(func)
    par = list(sig.parameters)
    for overload in get_overloads(func):
        ann = get_type_hints(overload)
        types = tuple(map(ann.get, par))
        registry[types] = overload

    def wrapper(*args, **kwargs):
        ba = sig.bind(*args, **kwargs)
        classes = tuple(ba.arguments[name].__class__ for name in par)
        overload = _dispatch(classes, registry)
        if not overload:
            given = '", "'.join(cls.__name__ for cls in classes)
            raise TypeError(f'No overload variant of "{func.__name__}" '
                            f'matches argument types "{given}"')
        return overload(*args, **kwargs)

    return cast(F, wrapper)

  1. Somewhat, because there is probably no good way to implement support for generic types. ↩︎

1 Like