How do you annotate the type of an abstract class with mypy?

I'm writing a library where I need a method that takes a (potentially) abstract type, and returns an instance of a concrete subtype of that type:

# script.py
from typing import Type
from abc import ABC, abstractmethod


class AbstractClass(ABC):
    @abstractmethod
    def abstract_method(self):
        pass

T = TypeVar('T', bound=AbstractClass)

def f(c: Type[T]) -> T:
    # find concrete implementation of c based on
    # environment configuration
    ...


f(AbstractClass)  # doesn't type check

Running mypy script.py yields:

error: Only concrete class can be given where "Type[AbstractClass]" is expected

I don't understand this error message and am having a hard time finding any documentation for it. Is there any way to annotate the function so that mypy will type check this?

As a side note, PyCharm's type checker, which is what I use the most, type checks f with no errors.

Upvotes: 21

Views: 15160

Answers (2)

Bluehorn
Bluehorn

Reputation: 3121

There exists a github issue about this misbehaviour (IMHO) in mypy. Basically, Type[_T] with _T being a TypeVar will never accept an abstract class.

The only sane solution I have seen is disabling this error, for example by including this in the mypy.ini file:

[mypy]
# Allows Type[T] to refer to abstract classes, which is not otherwise supported.
# See https://github.com/python/mypy/issues/4717
disable_error_code = type-abstract

Quoting from the discussion:

Now that #14619 was merged, would disabling the type-abstract error code by default address all the main issues, or is there something else that would need to be done?

I'll add my 2¢ to the ticket later and hope, they will iron this out.

Upvotes: 5

chadrik
chadrik

Reputation: 3480

It does appear that mypy is a bit biased against using an abstract base class this way, though as you demonstrate there are valid use cases.

You can work around this by making your factory function a class method on your abstract class. If stylistically you'd like to have a top-level function as a factory, then you can create an alias to the class method.

from typing import TYPE_CHECKING
from abc import ABC, abstractmethod


class AbstractClass(ABC):
    @abstractmethod
    def abstract_method(self):
        raise NotImplementedError

    @classmethod
    def make_concrete(cls) -> 'AbstractClass':
        """
        find concrete implementation based on environment configuration
        """
        return A()


class A(AbstractClass):
    def abstract_method(self):
        print("a")

# make alias
f = AbstractClass.make_concrete
x = f()
if TYPE_CHECKING:
    reveal_type(x)  # AbstractClass

Note that, without more work, mypy cannot know which concrete class is created by the factory function, it will only know that it is compatible with AbstractClass, as demonstrated by the output of reveal_type.

Alternately, if you're willing to give up the runtime checking provided by abc.ABC, you can get something even closer to your original design:

from typing import TYPE_CHECKING
from abc import abstractmethod


class AbstractClass:  # do NOT inherit from abc.ABC
    @abstractmethod
    def abstract_method(self):
        raise NotImplementedError


class A(AbstractClass):
    def abstract_method(self):
        print("a")


class Bad(AbstractClass):
    pass


def f() -> AbstractClass:
    """
    find concrete implementation based on environment configuration
    """
    pass

b = Bad()  # mypy displays an error here:  Cannot instantiate abstract class 'Bad' with abstract attribute 'abstract_method'

x = f()
if TYPE_CHECKING:
    reveal_type(x)  # AbstractClass

This works because mypy checks methods marked with @abstractmethod even if the class does not inherit from abc.ABC. But be warned that if you execute the program using python, you will no longer get an error about instantiating the Bad class without implementing its abstract methods.

Upvotes: 7

Related Questions