Search code examples
pythonabstract-classpython-3.5abc

Abstract Property on __name__ not enforced


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?


Solution

  • 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.