Python Negatypes
Back in 2007 Python added Abstract Base Classes, which were intended to be used as interfaces:
from abc import ABC
class AbstractIterable(ABC):
@abstractmethod
def __iter__(self):
while False:
yield None
def get_iterator(self):
return self.__iter__()
ABCs were added to strengthen the duck typing a little. If you inherited AbstractIterable
, then everybody knew you had an implemented __iter__
method, and could handle that appropriately.
Unsurprisingly, this idea never caught on. People instead prefered “better ask forgiveness than permission” and wrapped calls to __iter__
in a try block. This could be useful for static type checking, but in practice Mypy doesn’t use it. What if you wanted to typecheck it had __iter__
but the person did not inherit from AbstractIterable
? The Mypy team instead uses protocols, which is bootstrapped off ABCs but hides that detail from the user.
But ABC was intended to be backwards compatible. And there were already existing classes that had a iter
method. How could we include them under our AbstractIterable
ABC? To handle this, the Python team added a special ABC method:
class AbstractIterable(ABC):
@classmethod
def __subclasshook__(cls, C):
return hasattr(C, "__iter__")
__subclasshook__
is the runtime conditions that makes something count as a child of this ABC. isinstance(OurClass(), AbstractIterable)
is true if OurClass
has a iter
attribute, even if it didn’t inherit from AbstractIterable
.
That function is a runtime function. We can put arbitrary code in it. It passes in the object’s class, not the object itself, so we can’t inspect the properties of the specific object. We can still do some weird stuff:
class PalindromicName(ABC):
@classmethod
def __subclasshook__(cls, C):
name = C.__name__.lower()
return name[::-1] == name
Any class with a palindromic name, like “Noon”, will counts as a child class of PalindromicName
. We can push this even further: why gaze into the abyss when you can jump in?
class NotIterable(ABC):
@classmethod
def __subclasshook__(cls, C):
return not hasattr(C, "__iter__")
This is the type of everything that isn’t iterable. We have isinstance(5, NotAString)
, etc. We’ve created a negatype: a type that only specifies what it isn’t. We can include this as part of a set of positive types, subtracting out a subset of a given type. There’s nothing stopping us from making an ABC for “iterables that aren’t strings”, or “callable objects that don’t return an object of the same callable”.
How is this useful?
No idea.
ABCs aren’t checked as part of the method resolution order, so you can’t use this to patch in properties. Mypy can’t check __subclasshook__
. If you want it for runtime-checks, writing a function would be simpler and more portable than creating an ABC. Just about the only case where there is a difference is with single-dispatch functions, which can dispatch on virtual ABCs. But that’s about it.
It’s pretty cool, though!