Search code examples
pythonpython-typingmypystructural-typing

How to use __subclasshook__ with Mypy?


How come that under Mypy, __subclasshook__ works for one-trick ponies from collections.abc, but not for user-defined classes?

For instance, this program

from collections.abc import Hashable

class A:
    def __hash__(self) -> int:
        return 0

a: Hashable = A()

outputs

$ mypy demo.py --strict
Success: no issues found in 1 source file

But this equivalent program

from abc import ABCMeta, abstractmethod

def _check_methods(C: type, *methods: str) -> bool:
    mro = C.__mro__
    for method in methods:
        for B in mro:
            if method in B.__dict__:
                if B.__dict__[method] is None:
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

class Hashable(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __hash__(self) -> int:
        return 0

    @classmethod
    def __subclasshook__(cls, C: type) -> bool:
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

class A:
    def __hash__(self) -> int:
        return 0

a: Hashable = A()

outputs

$ mypy demo.py --strict
demo.py:32: error: Incompatible types in assignment (expression has type "A", variable has type "Hashable")
Found 1 error in 1 file (checked 1 source file)

Does Mypy handle one-trick ponies in a special way?


Solution

  • Mypy does not use the implementations of the standard library but its specifications (’stub files’) from the typeshed package. In this package, collections.abc.Hashable is a typing.Protocol.

    typeshed/stdlib/_collections_abc.pyi:

    from typing import (
        AbstractSet as Set,
        AsyncGenerator as AsyncGenerator,
        AsyncIterable as AsyncIterable,
        AsyncIterator as AsyncIterator,
        Awaitable as Awaitable,
        ByteString as ByteString,
        Callable as Callable,
        Collection as Collection,
        Container as Container,
        Coroutine as Coroutine,
        Generator as Generator,
        Generic,
        Hashable as Hashable,
        ItemsView as ItemsView,
        Iterable as Iterable,
        Iterator as Iterator,
        KeysView as KeysView,
        Mapping as Mapping,
        MappingView as MappingView,
        MutableMapping as MutableMapping,
        MutableSequence as MutableSequence,
        MutableSet as MutableSet,
        Reversible as Reversible,
        Sequence as Sequence,
        Sized as Sized,
        TypeVar,
        ValuesView as ValuesView,
    )
    

    typeshed/stdlib/typing.pyi:

    @runtime_checkable
    class Hashable(Protocol, metaclass=ABCMeta):
        # TODO: This is special, in that a subclass of a hashable class may not be hashable
        #   (for example, list vs. object). It's not obvious how to represent this. This class
        #   is currently mostly useless for static checking.
        @abstractmethod
        def __hash__(self) -> int: ...