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?
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,
)
@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: ...