This is based on my first review and the suggested points in Multiple dispatch decorator in Python:import inspectfrom functools import wraps, update_wrapper, partialclass multi_dispatch(object):        Returns a multiple dispatch version of the function, that has a register    method with which new versions of the function can be registered. These    functions each must have annotated parameters with the types they shall be    called with. Keyword parameters and self must not be annotated.    The decorated function works by searching for an exact match for the types    of the given arguments. If no match is found, the function on which this    decorator has been used, is called. If you don't want this, raise a    TypeError inside the function.    >>> FMT = 'In {} the {} and {} collide.'    >>> FMT_SA = 'In {} the {} is smashed to bits by {}.'    >>> FMT_AS = 'In {} the {} is hit by the {}.'    >>>    >>> class GameObject(object):    ...     def __init__(self, name):    ...         self.name = name    ...    ...     def __repr__(self):    ...         return self.name    ...    ...    >>> class Asteroid(GameObject):    ...     pass    ...    ...    >>> class Spaceship(GameObject):    ...     pass    ...    ...    >>> class Starcluster(GameObject):    ...     @multi_dispatch    ...     def collide(self, a, b):    ...         print(FMT.format(self, a, b))    ...    ...     @collide.register    ...     def collide(self, a: Spaceship, b: Asteroid):    ...         print(FMT_SA.format(self, a, b))    ...    ...     @collide.register    ...     def collide(self, a: Asteroid, b: Spaceship):    ...         print(FMT_AS.format(self, a, b))    ...    ...    >>> a = Asteroid('Iris')    >>> s = Spaceship('Voyager')    >>> sc = Starcluster('Messier 69')    >>>    >>> sc.collide(a, s)    In Messier 69 the Iris is hit by the Voyager.    >>> sc.collide(s, 'a grain of dust')    In Messier 69 the Voyager and a grain of dust collide.    >>>    >>>    >>> @multi_dispatch    ... def collide(a, b):    ...     print(FMT.format('outer space', a, b))    ...    ...    >>> @collide.register    ... def collide(a: Spaceship, b: Asteroid):    ...     print(FMT_SA.format('outer space', a, b))    ...    ...    >>> @collide.register    ... def collide(a: Asteroid, b: Spaceship):    ...     print(FMT_AS.format('outer space', a, b))    ...    ...    >>> collide(a, s)    In outer space the Iris is hit by the Voyager.    >>> collide(s, a)    In outer space the Voyager is smashed to bits by Iris.    >>> collide(s, 'a grain of dust')    In outer space the Voyager and a grain of dust collide.        def __init__(self, function):        update_wrapper(self, function)        self.default_function = function        self._annotation_to_function = {}        self._is_bound_method = False    def __get__(self, obj, objtype):        self._is_bound_method = True        return partial(self.__call__, obj)    def register(self, function):        annotation_to_function = self._annotation_to_function        annotation = self._get_annotations(function)        if annotation in annotation_to_function:            raise TypeError(duplicate registration of:  +                            str(annotation))        annotation_to_function[annotation] = function        return self    def _get_annotations(self, function, empty=inspect.Parameter.empty):        parameters = inspect.signature(function).parameters        annotations = (a.annotation for a in parameters.values())        return tuple(i for i in annotations if i is not empty)    def __call__(self, *args, **kwargs):        annotation_to_function = self._annotation_to_function        types = tuple(type(a) for a in args)        if self._is_bound_method:            types = types[1:]        if types not in annotation_to_function:            return self.default_function(*args, **kwargs)        return annotation_to_function[types](*args, **kwargs)class inheritance_multi_dispatch(multi_dispatch):        >>> FMT = 'In {} the {} and {} collide.'    >>> FMT_SA = 'In {} the {} is smashed to bits by {}.'    >>> FMT_AS = 'In {} the {} is hit by the {}.'    >>>    >>> class GameObject(object):    ...     def __init__(self, name):    ...         self.name = name    ...    ...     def __repr__(self):    ...         return self.name    ...    ...    >>> class Asteroid(GameObject):    ...     pass    ...    ...    >>> class Spaceship(GameObject):    ...     pass    ...    ...    >>> class Starcluster(GameObject):    ...     @inheritance_multi_dispatch    ...     def collide(self, a, b):    ...         print(FMT.format(self, a, b))    ...    ...     @collide.register    ...     def collide(self, a: (Spaceship, Asteroid), b: str):    ...         print(FMT_SA.format(self, a, b))    ...    ...     @collide.register    ...     def collide(self, a: Asteroid, b: Spaceship):    ...         print(FMT_AS.format(self, a, b))    ...    ...    >>> a = Asteroid('Iris')    >>> s = Spaceship('Voyager')    >>> sc = Starcluster('Messier 69')    >>>    >>> sc.collide(a, 'a teapot')    In Messier 69 the Iris is smashed to bits by a teapot.    >>> sc.collide(s, 'a teapot')    In Messier 69 the Voyager is smashed to bits by a teapot.    >>> sc.collide(a, s)    In Messier 69 the Iris is hit by the Voyager.    >>> sc.collide('Moon', 'a stone')    In Messier 69 the Moon and a stone collide.        def __init__(self, function):        self._cache = {}        self.set_argument_to_annotation_matcher(isinstance)        super().__init__(function)    def set_argument_to_annotation_matcher(self, matcher):        self._matcher = matcher    def register(self, function):        self._cache = {}        return super().register(function)    def __call__(self, *args, **kwargs):        cache = self._cache        types = tuple(type(a) for a in args)        if self._is_bound_method:            types = types[1:]        try:            method = cache[types]        except KeyError:            cache[types] = method = self._find_match(args)        return method(*args, **kwargs)    def _find_match(self, args):        annotation_to_function = self._annotation_to_function        matcher = self._matcher        if self._is_bound_method:            args = args[1:]        for annotations, function in annotation_to_function.items():            if all(matcher(arg, ann) for arg, ann in zip(args, annotations)):                return function        return self.default_functionAgain, I'd like to read what you think about the code from every point of view!
Multiple dispatch decorator classes in Python
python;python 3.x;meta programming;overloading
The code itself looks quite pretty and overall readable, considering that the topic itself requires some knowledge about the Python language internals. Some suggestions in descending order of urgency:You should either document that @classmethod and @staticmethod are not supported, or implement support for them.It might be sensible to add a sanity-check in case someone annotates a keyword argument.I prefer to use formats instead of string concatenation, even for exception messages: raise TypeError(duplicate registration of: {}.format(                    annotation))This allows for easier extension of the error message (in case it turns out to be too shallow at some point; one might want to include the overloaded functions name at some point in the future) and makes it easier to read.Explicitly inheriting from object is not required anymore in Python 3, but it is not wrong to explicitly specify that.