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.