Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

abstract dataclass inheritance gives Only concrete class can be given where "Type[Abstract]" is expected #5374

Closed
ojii opened this issue Jul 19, 2018 · 39 comments · Fixed by #13398
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code needs discussion priority-0-high topic-dataclasses

Comments

@ojii
Copy link

ojii commented Jul 19, 2018

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.

@gvanrossum
Copy link
Member

gvanrossum commented Jul 19, 2018 via email

@ojii
Copy link
Author

ojii commented Jul 19, 2018

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

@ilevkivskyi
Copy link
Member

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.

@ilevkivskyi ilevkivskyi added bug mypy got something wrong needs discussion priority-1-normal false-positive mypy gave an error on correct code labels Jul 19, 2018
@antdking
Copy link

antdking commented Nov 6, 2018

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: ...

@ilevkivskyi
Copy link
Member

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.

@JukkaL
Copy link
Collaborator

JukkaL commented Nov 8, 2018

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.

@ilevkivskyi
Copy link
Member

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.

@ZeeD
Copy link

ZeeD commented Mar 23, 2019

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

@gvanrossum
Copy link
Member

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

@nickwilliams-eventbrite
Copy link

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.

@aldanor
Copy link

aldanor commented Oct 28, 2019

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?

@JukkaL
Copy link
Collaborator

JukkaL commented Oct 28, 2019

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

@ToBeReplaced
Copy link

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"

@twhaples
Copy link

twhaples commented Feb 4, 2020

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())

@schuderer
Copy link

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.

@jdlourenco
Copy link

Running into the same issue.

@AlexanderPodorov
Copy link

For anyone reading this, I've solved it by getting rid of abstractmethod, we can just throw exception in the property:

@property
    def prop(self) -> Any:
        raise NotImplemented

We lose IDE check here, because we can instantiate a class, but calling the property at runtime will throw the exception, which should be fine in most of cases.

@TheEdgeOfRage
Copy link

@AlexanderPodorov It kind of defeats the purpose of type hinting and static checks if you rely on runtime errors. The point of @abstractmethod is to be able to tell statically when somebody tries to instantiate an abstract class.

jevandezande added a commit to jevandezande/spectra that referenced this issue Mar 8, 2022
    Formerly abstract, `Spectrum` has to be made concrete due to
    [mypy issue#5374](python/mypy#5374)
    `Only concrete class can be given where "Type[Abstract]" is expected`
@chrishavlin
Copy link

chrishavlin commented Mar 17, 2022

I'm seeing the same issue with an abstract class wrapped by @functools.total_ordering. If you have something of the form:

@functools.total_ordering
class Student(abc.ABC):
    << cutting total_ordering implementation >> 
    
    @abc.abstract_method
    def get_record(self):
        pass
    
class NewStudent(Student):
    def get_record(self) -> str:
        return f"{self.lastname}, {self.firstname}"

it fails mypy with error: Only concrete class can be given where "Type[Student]" is expected. Full reproducible example here (not worth cluttering the discussion here with the total_ordering implementation). It only errors if both total_ordering and abstract_method are used. (Edit: I'm fine adding type: ignore[misc] to the @functools.total_ordering line, just thought it was worth mentioning the occurrence in this thread).

@gitpushdashf
Copy link

Hi! Does anyone have an update or simple workaround for this?

@kokumura
Copy link

Is there any further discussions about this use case?

antdking commented on 7 Nov 2018

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: ...

@Rahix
Copy link

Rahix commented Sep 28, 2022

@kokumura, I think the correct issue for this topic is #4717. Still unresolved, though.

@gh-andre
Copy link

This error assumes a specific usage of the passed in type, which may be common, but still not the only way a passed in class can be used, so it is an incorrect assumption. The actual error, which is trying to instantiate an abstract class is not reported in this case.

The example in this comment highlights this assumption (#5374 (comment)):

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 may have a method that reads YAML configuration that checks the expected types for returned values or the code may call static/class methods for passed in types, etc.

Here's a simplified example. In actual code, B could be Mapping or MutableMapping and get_origin would be used to figure out the underlying types or class methods could be used in a meaningful way.

from typing import TypeVar, Type
from abc import abstractmethod
class B:
    @classmethod
    def cm(cls, i: int) -> str:
        return str(i) + "_ABC"
        
    @abstractmethod
    def am(self) -> int: ...

class X(B):
    @classmethod
    def cm(cls, i: int) -> str:
        return str(i) + "_XYZ"
        
    def am(self) -> int:
        return 123

T = TypeVar("T", bound=B)

def fn(c: Type[T]) -> int|str:
    print(c.cm(222))
    return 1 if c == B else "ABC"

assert(fn(B) == 1)     # error: Only concrete class can be given where
                       # "Type[B]" is expected  [type-abstract]
assert(fn(X) == "ABC")

A useful error would be if the code within the flagged function actually tries to call a constructor for an abstract type, not that an abstract type was passed in. It is unfortunate that this issue was closed.

@NeilGirdhar
Copy link
Contributor

NeilGirdhar commented May 26, 2023

It is unfortunate that this issue was closed.

@gh-andre The issue discussing leaving/removing the error for passing abstract types to functions that accept types is #4717.

@gh-andre
Copy link

@NeilGirdhar Thanks for pointing out the new issue. Will keep an eye on it.

jwodder added a commit to jwodder/demagnetize that referenced this issue Jun 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code needs discussion priority-0-high topic-dataclasses
Projects
None yet
Development

Successfully merging a pull request may close this issue.