Mypy: abstract dataclass inheritance gives `Only concrete class can be given where "Type[Abstract]" is expected`

Created on 19 Jul 2018  Â·  18Comments  Â·  Source: python/mypy

mypy 0.620, --strict.

Sample code:

import abc
from dataclasses import dataclass


@dataclass  # error: Only concrete class can be given where "Type[Abstract]" is expected
class Abstract(metaclass=abc.ABCMeta):
    a: str

    @abc.abstractmethod
    def c(self) -> None:
        pass


@dataclass
class Concrete(Abstract):
    b: str

    def c(self) -> None:
        pass


instance = Concrete(a='hello', b='world')

Note that this error only occurs if at least one abstract method is defined on the abstract class. Removing the abstract method c (or making it non-abstract) makes the error go away.

bug false-positive needs discussion priority-0-high

Most helpful comment

Marking as high priority, since there seems to be quite a lot of interest in getting this fixed.

All 18 comments

What if you remove the meta class?

On Wed, Jul 18, 2018 at 9:01 PM Jonas Obrist notifications@github.com
wrote:

mypy 0.620, --strict.

Sample code:

import abcfrom dataclasses import dataclass

@dataclass # error: Only concrete class can be given where "Type[Abstract]" is expectedclass Abstract(metaclass=abc.ABCMeta):
a: str

@abc.abstractmethod
def c(self) -> None:
    pass

@dataclassclass Concrete(Abstract):
b: str

def c(self) -> None:
    pass

instance = Concrete(a='hello', b='world')

Note that this error only occurs if at least one abstract method is
defined on the abstract class. Removing the abstract method c (or making
it non-abstract) makes the error go away.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/python/mypy/issues/5374, or mute the thread
https://github.com/notifications/unsubscribe-auth/ACwrMmnqdQS-PT-Ppma35aXOsPpD6X7vks5uIASAgaJpZM4VVt0v
.

>

--Guido (mobile)

Removing the metaclass still triggers it. Minimal code to get that error:

import abc
from dataclasses import dataclass

@dataclass
class Abstract:
    @abc.abstractmethod
    def c(self) -> None:
        pass

The underlying cause is this:

from typing import Type, TypeVar
from abc import abstractmethod

T = TypeVar('T')

class C:
    @abstractmethod
    def f(self) -> None: ...

def fun(x: Type[T]) -> Type[T]: ...

fun(C)

A possible simple solution is to type dataclass in typeshed as roughly (S) -> S with S = TypeVar('S', bound=Type[Any]). Another option is to consider relaxing the restriction that mypy has for Type[T] with abstract classes, but I am not sure where to draw the line.

Lifting this constraint would be great for fetching implementations to interfaces (abstracts/protocols).

T_co = TypeVar('T_co', covariant=True)

def register(interface: Type[T_co], implementation: Type[T_co]) -> Type[T_co]: ...
def inject(interface: Type[T_co]) -> T_co: ...

Just a bit of context why this is prohibited: mypy always allows _instantiation_ of something typed as Type[...], in particular of function arguments, for example:

from typing import Type, TypeVar
from abc import abstractmethod

T = TypeVar('T')

class C:
    @abstractmethod
    def f(self) -> None: ...

def fun(x: Type[T]) -> T: ...
    return x()  # mypy allows this, since it is a very common pattern

fun(C)  # this fails at runtime, and it would be good to catch it statically,
        # what we do currently

I am still not sure what is the best way to proceed here. Potentially, we can only prohibit this if original function argument (or its upper bound) is abstract (so function actually expects implementations to be passed). This will be a bit unsafe, but more practical, as all examples provided so far would pass.

Another option is to consider relaxing the restriction that mypy has for Type[T] with abstract classes, but I am not sure where to draw the line.

I'm leaning towards allowing abstract classes to be used without restriction. Type[x] is already unsafe since __init__ can be overriden with a different signature, and I expect that the extra unsafety from allowing abstract classes would be marginal in practice. We just need to document the potential issues clearly.

I'm leaning towards allowing abstract classes to be used without restriction. [...] We just need to document the potential issues clearly.

This will be false negative, so probably OK.

Sorry to bother, but is there some ETA on this issue?

There is no concrete work planned for this issue yet, we are focusing on high-priority issues.

I'm having a similar error and I don't know if it's the same problem or a different, related problem (or perhaps I'm doing something wrong), related to abstract classes and Attrs.

I have this abstract class: https://github.com/eventbrite/pymetrics/blob/ab608978/pymetrics/recorders/base.py#L38-L39

Then I have this constant:

_DEFAULT_METRICS_RECORDER = NoOpMetricsRecorder()  # type: MetricsRecorder

And this Attrs class:

@attr.s
@six.add_metaclass(abc.ABCMeta)
class RedisTransportCore(object):
    ...
    metrics = attr.ib(
        default=_DEFAULT_METRICS_RECORDER,
        validator=attr.validators.instance_of(MetricsRecorder),
    )  # type: MetricsRecorder

On the line with attr.validators.instance_of, I'm getting this error:

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

What's interesting is that this only happens with today's released Attrs 19.2.0 and last week's released MyPy 0.730. If I downgrade to Attrs 19.1.0 and keep the latest MyPy, the error goes away. If I downgrade to MyPy 0.720 and keep the latest Attrs, the error goes away.

I'm able to get around the problem by adding # type: ignore at the end of the line with attr.validators.instance_of, so I'm good for now, but I would like to know if I need to file a separate issue or if this is related or if this is a bug with Attrs.

This is really sad, given that dataclasses are being pushed as a go-to class container in 3.7+, having abstract dataclasses is not such a rare occasion. But mypy in the current state basically prohibits using it.

Given that this issue is 1.5 years old, wonder if there's any known workarounds?

Marking as high priority, since there seems to be quite a lot of interest in getting this fixed.

Depending on your needs, for a temporary workaround, you can do the following, which type checks with --strict.

import abc
import dataclasses


class AbstractClass(abc.ABC):
    """An abstract class."""

    @abc.abstractmethod
    def method(self) -> str:
        """Do something."""


@dataclasses.dataclass
class DataClassMixin:
    """A dataclass mixin."""

    spam: str


class AbstractDataClass(DataClassMixin, AbstractClass):
    """An abstract data class."""


@dataclasses.dataclass
class ConcreteDataClass(AbstractDataClass):
    """A concrete data class."""

    eggs: str

    def method(self) -> str:
        """Do something."""
        return "{} tastes worse than {}".format(self.spam, self.eggs)


print(ConcreteDataClass("spam", "eggs").method())
# -> "spam tastes worse than eggs"

Similar to @cybojenix, we would be very happy if mypy could support service locator patterns similar to the following (the code for which works, but does not pass mypy type checks for want of a concrete class.)

from typing import Any, Dict, TypeVar, Type
from typing_extensions import Protocol

ServiceType = TypeVar("ServiceType")

registrations: Dict[Any, Any] = {}
def register(t: Type[ServiceType], impl: ServiceType) -> None:
    registrations[t] = impl

def locate(t: Type[ServiceType]) -> ServiceType:
    return registrations[t]


class ThingService(Protocol):
    def serve(str) -> str:
        pass

class ThingImpl(ThingService):
    def serve(str) -> str:
        return "fish"


register(ThingService, ThingImpl())
thing: ThingService = locate(ThingService)
print(thing.serve())

Interestingly, the given rationale of not allowing abstract classes ("mypy always allows instantiation of something typed as Type[...], in particular of function arguments"), does not seem to match its implementation, as classes based on abc.ABC cannot be instantiated, but code using only this actually pass the mypy check, whereas classes with @abstractmethods can be instantiated, but classes using those trigger the false positives discussed in this issue.

To elaborate (and give another example to possibly test a fix against later), the following code uses only @abstractmethods and triggers the false positive:

from abc import ABC, abstractmethod
from typing import List, Type, TypeVar

AB = TypeVar("AB", "A", "B")  # Could be A or B (Union)

class A:
    @abstractmethod
    def snork(self):
        pass
class B:
    @abstractmethod
    def snork(self):
        pass

class oneA(A):
    pass
class anotherA(A):
    pass
class someB(B):
    pass

def make_objects(of_type: Type[AB]) -> List[AB]:
    return [cls() for cls in of_type.__subclasses__()]

if __name__ == "__main__":
    print(make_objects(A))
    print(make_objects(B))

Removing the two @abstractmethod decorators and changing class A: and class B: to class A(ABC): and class B(ABC): actually passes mypy's check without errors.

Running into the same issue.

Similar to @ToBeReplaced, workaround I've been using:

@dataclass
class _AbstractDataclass:
    id: int

class AbstractDataclass(_AbstractDataclass, abc.ABC):
    @abc.abstractmethod
    def c(self) -> None:
        pass

Another use-case which also doesn't work due to this limitation is the adapter pattern using typing Protocols.

i.e.:

from typing import Protocol, TypeVar, Type

T = TypeVar('T')

class IFoo(Protocol):

    def foo(self) -> str:
        pass

class Adaptable(object):

    def get_adapter(self, class_: Type[T]) -> T:
        ...

def some_call(adaptable: Adaptable) -> None:
    foo = adaptable.get_adapter(IFoo)
Was this page helpful?
0 / 5 - 0 ratings