2026, Jan 03 07:00

Fixing NotImplementedError from abstract class properties in Python: descriptors, ABCMeta, and class creation

Learn why abstract class properties with descriptors and ABCMeta raise NotImplementedError during class creation, and how an abstract-aware descriptor fixes it.

Abstract class properties in Python can be made to work, but there is a subtle trap when combining descriptors, class properties, and ABCMeta. If you’ve ever seen a NotImplementedError fired long before your concrete subclass is instantiated, the issue likely emerges during class creation itself, not at runtime use.

Reproducing the issue

The following example defines a class-level property via a descriptor, and uses a metaclass that extends ABCMeta to avoid conflicts. Two abstract base classes are followed by a concrete subclass. Accessing the property on the concrete subclass unexpectedly triggers NotImplementedError.

from abc import ABCMeta, abstractmethod
from typing import Any


class TypeField[S, V](property):
    def __get__(self, obj: S, typ: type[S] = None) -> V:
        return self.fget(typ)


class MetaWithTypeField(ABCMeta):
    def __setattr__(mcls, key: str, val: Any):
        if isinstance(d := vars(mcls).get(key), TypeField) and not callable(d.fset):
            raise AttributeError("can't set attribute")
        return super().__setattr__(key, val)


class Alpha(metaclass=MetaWithTypeField):
    __slots__ = ()

    @TypeField
    @abstractmethod
    def KEY(klass):
        raise NotImplementedError


class Beta(Alpha):
    ...
    # @TypeField
    # def KEY(klass):
    #     return 1


class Gamma(Beta):
    @TypeField
    def KEY(klass):
        return 1


print(Gamma.KEY)
# result: NotImplementedError gets triggered

What’s really happening

The failure doesn’t originate from print(Gamma.KEY) as such. It happens while Python is creating the Beta class. During class creation, ABCMeta.__new__ has to determine which attributes are abstract. To do this, it accesses the descriptor KEY on Beta. That access invokes TypeField.__get__, which immediately calls the underlying function implementation. But at that point, Beta hasn’t provided an implementation, so the abstract method on Alpha runs and raises NotImplementedError.

This call path is visible in the stack trace: the access comes from the abc machinery during class initialization, not from your explicit print. The inspection step triggers the descriptor and therefore the abstract getter, causing the early exception.

The fix: make the descriptor aware of abstract methods

The property descriptor needs to differentiate between abstract and concrete access during class creation. If the getter is abstract for the class being inspected, it should return the function object itself rather than invoking it. Only when the class provides a concrete implementation should the descriptor call the getter.

class TypeField[S, V](property):
    def __get__(self, obj: S, typ: type[S] = None) -> V:
        if getattr(self.fget, "__isabstractmethod__", False) and typ is not None:
            if not hasattr(typ, "__abstractmethods__"):
                return self.fget
            if self.fget.__name__ in getattr(typ, "__abstractmethods__", set()):
                return self.fget
        return self.fget(typ)

With this change, when ABCMeta inspects the class, the descriptor returns the abstract function object instead of calling it. Once a subclass defines a concrete class property, the descriptor proceeds to call the implementation.

Why touching __abstractmethods__ can appear to “fix” it

There is a related observation: forcing access to getattr(owner, "__abstractmethods__") before returning in the original descriptor seems to make the error disappear. That happens because __abstractmethods__ on ABCMeta is computed lazily via a property; the first access triggers the calculation and caching. Once computed, ABCMeta no longer needs to probe each attribute to decide abstractness, so it stops poking descriptors in a way that would call the abstract getter.

Practical implications

This behavior means descriptors that execute code in __get__ can be invoked very early, during class body evaluation and metaclass processing. When those descriptors wrap abstract members, they must be careful to avoid executing the abstract callable. Making the descriptor abstract-aware prevents accidental evaluation during ABCMeta’s inspection phase. It is also worth noting another edge reported in practice: if a class-level property refers to a symbol that is defined later, accessing it too early can lead to NameError. This is a symptom of the same timing issue—access may occur during class creation.

Conclusion

Class properties implemented via descriptors play well with ABCMeta once the descriptor accounts for abstract methods. The key is to let abstract members pass through untouched during class creation and only invoke the getter when a concrete implementation exists on the class. If you observe that simply accessing __abstractmethods__ makes things “work,” remember you’re forcing ABCMeta’s lazy computation and avoiding further probing. Keep an eye on early access to names defined later in the module, and structure class-level initialization so that descriptors don’t evaluate prematurely.