pythonpython-typingmypy

How to mark a class as abstract in python (no abstract methods and in a mypy compatible, reusable way)?


I'm trying to make it impossible to instantiate a class directly, without it having any unimplemented abstract methods.

Based on other solutions online, a class should have something along the lines of:

class Example:
    def __new__(cls,*args,**kwargs):
        if cls is Example:
            raise TypeError("...")
        return super().__new__(cls,*args,**kwargs)

I'm trying to move this snippet to a separate place such that each such class does not have to repeat this code.

C = TypeVar("C")
def abstract(cls:Type[C])->Type[C]:
    class Abstract(cls):
        def __new__(cls, *args:Any, **kwargs:Any)->"Abstract":
            if cls is Abstract:
                msg = "Abstract class {} cannot be instantiated".format(cls.__name__)
                raise TypeError(msg)
            return cast( Abstract, super().__new__(*args,**kwargs) )

    return Abstract

This is my attempt and might be incorrect

But mypy complains:

error: Invalid base class "cls"

How can I have some reusable way (such as a decorator) to achieve what I want whilst passing mypy --strict?

Context:

This is in the context of a pyside6 application where I'm subclassing QEvent, to have some additional extra properties. The base class defining these properties (getters) has a default implementation, yet I would like to prevent it from being initialized directly as it is not (and should not) be registered to the Qt event system. (I have a couple more such classes with different default values for convenience)


Solution

  • May I suggest a mixin instead of a decorator? You basically want to check if cls.mro()[1] is the abstract class (i.e., in the current class is a direct subclass of Abstract)

    from typing import Self, final
    
    class Abstract:
        @final
        def __new__(cls, *args, **kwargs) -> Self:
            if cls.mro()[1] is Abstract:
                raise TypeError("...")
            return super().__new__(cls,*args,**kwargs)
    
    class AbstractFoo(Abstract):
        def frobnicate(self, x: int) -> int:
            return x // 42
    
    class ConcreteFoo(AbstractFoo):
        def __init__(self, value: int) -> None:
            self.value = value
    
    class AbstractBaz(Abstract):
        def __new__(cls, *args, **kwargs) -> Self:
            return object.__new__(cls)
    
    foo1 = ConcreteFoo(1)
    foo2 = AbstractFoo() # TypeError: ...
    

    Note, due to the @final decorator on Abstract.__new__, mypy complains about trying to override __new__ in AbstractBaz:

    main.py:20: error: Cannot override final attribute "__new__" (previously declared in base class "Abstract")  [misc]
    

    You may or may not want to go with this, depending on how much you want to control, but keep in mind, if someone really wants to instantiate your class, they can and you cannot really stop them because any user can simply use object.__new__ directly.

    Note, static analysis tools will not catch:

    foo2 = AbstractFoo()
    

    I don't think there is any way to express abstractness in the type system itself, abc is special cased.