19

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.

  • Your example doesn't make any sense to me: `f` is not even using its argument. Can you please clarify what you are trying to achieve? If your goal is to pointlessly pass an abstract class to a function then, yes, mypy will raise an error, and until you can demonstrate a reason to do this, then I have to agree that it's the right behavior. – chadrik Feb 01 '18 at 20:10
  • You've declared that the type of the argument passed in to `f` is the same as the type of the instance returned from `f`, but then you pass in an `AbstractClass` and expect to get back a `ConcreteClass` instance? Flesh out the example a bit more. Store the result of `f(AbstractClass)` and describe what you think the type should be. It sounds like you want mypy to think that the result is a `AbstractClass` instance, while at runtime its actually a `ConcreteClass` instance, but I'm still pretty unclear on your expectations. – chadrik Feb 01 '18 at 20:25
  • The specific use case I have is to have `f` behave as a factory method/dependency injection mechanism based on some environment configuration. The totality of it is a little too involved to put here, created a (simplifed) gist instead https://gist.github.com/suned/321725213d81065ac5c22e734ebd9d9e. – Sune Andreas Dybro Debel Feb 01 '18 at 20:35
  • ok, that makes more sense. I came up with something that produces the desired result. – chadrik Feb 02 '18 at 20:21

2 Answers2

8

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.

chadrik
  • 3,413
  • 1
  • 21
  • 19
  • Thanks for you answer. You solution however is exactly what I had in my original code: I simplified it for Stack Overflow to produce a minimum reproducing example. The code in your answer outputs the same error as in my original post. – Sune Andreas Dybro Debel Feb 01 '18 at 10:47
  • No it's actually not. I added a concrete class, passed that to `f`, and used a `TypeVar` to ensure that the type passed to `f` is reflected in the result (I'll add some more to the example to clarify that last part). As a result my code runs in python3 and passes in mypy, whereas yours fails in both. (Note: I forgot to add a return statement to `f` in the complete example which I corrected). – chadrik Feb 01 '18 at 17:32
  • Sorry, I was unclear: I also use a type var in the code that prompted me to write this post in the first place. I simplified the example for stack overflow. My mistake, I didn't notice the concrete class. That doesn't solve my problem unfortunately since I really do want to pass an abstract class. My use case is something akin to a factory method where you pass a type and is returned a (concrete) subtype of that type. – Sune Andreas Dybro Debel Feb 01 '18 at 19:47
  • Can you please improve the example code so that it demonstrates your problem. – chadrik Feb 01 '18 at 19:48
  • I updated my response to include a solution that is closer to what the OP was originally looking for, though with some caveats – chadrik Apr 16 '18 at 18:33
3

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.

Bluehorn
  • 2,956
  • 2
  • 22
  • 29