Reputation: 503
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
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
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