Consider the following sample code:
from abc import ABC, abstractmethod, abstractproperty
class Base(ABC):
@abstractmethod
def foo(self) -> str:
print("abstract")
@property
@abstractmethod
def __name__(self) -> str:
return "abstract"
@abstractmethod
def __str__(self) -> str:
return "abstract"
@property
@abstractmethod
def __add__(self, other) -> str:
return "abstract"
class Sub(Base):
def foo(self):
print("concrete")
def __str__(self):
return "concrete"
def __add__(self, other) -> str:
return "concrete"
sub = Sub()
sub.foo()
sub.__name__
print(str(sub))
Note that the subclass does not implement the abstract property __name__
, and indeed when __name__
is referenced, it prints as "abstract" from its parent:
>>> sub.foo()
concrete
>>> sub.__name__
'abstract'
>>> print(str(sub))
concrete
However, it is not because __name__
is a dunder method, nor because of some issue with @property
and @abstractmethod
decorators not working well together, because if I remove the implementation of __add__
from Sub
, it does not let me instantiate it. (I know __add__
is not normally a property, but I wanted to use a 'real' dunder method) The same expected behavior occurs if I remove the implementation of __str__
and foo
. Only __name__
behaves this way.
What is it about __name__
that causes this behavior? Is there some way around this, or do I need to have the parent (abstract) implementation manually raise the TypeError
for me?
Classes have a __name__
attribute by way of a data descriptor on type
:
>>> Sub.__name__
'Sub'
>>> '__name__' in Sub.__dict__
False
It's a data descriptor because it also intercepts assignments to ensure that the value is a string. The actual values are stored in a slot on the C structure, the descriptor is a proxy for that value (so setting a new value on the class doesn't add a new entry to the __dict__
either):
>>> Sub.__name__ = None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign string to NewName.__name__, not 'NoneType'
>>> Sub.__name__ = 'NewName'
>>> Sub.__name__
'NewName'
>>> '__name__' in Sub.__dict__
False
(actually accessing that descriptor without triggering it's __get__
is not really possible as type
itself has no __dict__
and has itself a __name__
).
This causes the test for the attribute when creating instances of Sub
to succeed, the class has that attribute after all:
>>> hasattr(Sub, '__name__')
True
On instances of Sub
, the Base.__name__
implementation is then found because instance descriptor rules only consider the class and base classes, not the metatype.