Skip to content

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

Closed

Description

@ojii

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.

Activity

  1. gvanrossum commented on Jul 19, 2018

    @gvanrossum
    Member
  2. ojii commented on Jul 19, 2018

    @ojii
    Author

    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
  3. ilevkivskyi commented on Jul 19, 2018

    @ilevkivskyi
    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.

  4. antdking commented on Nov 6, 2018

    @antdking

    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: ...
  5. ilevkivskyi commented on Nov 7, 2018

    @ilevkivskyi
    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.

  6. JukkaL commented on Nov 8, 2018

    @JukkaL
    Collaborator

    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.

  7. ilevkivskyi commented on Nov 8, 2018

    @ilevkivskyi
    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.

  8. ZeeD commented on Mar 23, 2019

    @ZeeD

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

  9. gvanrossum commented on Mar 23, 2019

    @gvanrossum
    Member

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

  10. nickwilliams-eventbrite commented on Oct 1, 2019

    @nickwilliams-eventbrite

    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.

  11. aldanor commented on Oct 28, 2019

    @aldanor

    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?

  12. 35 remaining items

  13. chrishavlin commented on Mar 17, 2022

    @chrishavlin

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

  14. gitpushdashf commented on Jul 29, 2022

    @gitpushdashf

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

  15. added a commit that references this issue on Aug 12, 2022
    52f1dd3
  16. kokumura commented on Sep 28, 2022

    @kokumura

    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: ...
  17. Rahix commented on Sep 28, 2022

    @Rahix

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

  18. gh-andre commented on May 26, 2023

    @gh-andre

    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.

  19. NeilGirdhar commented on May 26, 2023

    @NeilGirdhar
    Contributor

    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.

  20. gh-andre commented on May 26, 2023

    @gh-andre

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

  21. added a commit that references this issue on Jun 11, 2023
  22. locked as resolved and limited conversation to collaborators on May 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions