1. A problem with dataclass mix-ins, solved
To make abstract dataclasses that type-check under mypy, I've been breaking them into two classes, one that contains the abstract methods and one that contains the data members, as explained in this answer. The abstract class inherits from the dataclass. This runs into a problem, though, when another abstract-class-and-dataclass pair inherits from the first one: the "ancestor" dataclass's fields get wiped out by the "descendant". For example:
from dataclasses import dataclass
from abc import ABC, abstractmethod
#dataclass
class ADataclassMixin:
a_field: int = 1
class A(ADataclassMixin, ABC):
#abstractmethod
def method(self):
pass
#dataclass
#class BDataclassMixin(A): # works but fails mypy 0.931 type-check
class BDataclassMixin: # fails
b_field: int = 2
pass
class B(BDataclassMixin, A):
def method(self):
return self
o = B(a_field=5)
The last line fails, yielding this error message:
TypeError: BDataclassMixin.__init__() got an unexpected keyword argument 'a_field'
B's method-resolution order (B.__mro__) is (B, BDataclassMixin, A, ADataclassMixin, ABC, object), as expected. But a_field is not found.
A solution, shown in the commented-out line above, is to put the ancestor class explicitly in the descendant dataclass's declaration: class BDataclassMixin(A) instead of class BDataclassMixin. This fails type-checking, though, because a dataclass can only be a concrete class.
2. A problem with that solution, unsolved
The above solution breaks down if we add a third class, inheriting from B:
#dataclass
#class CDataclassMixin: # fails
class CDataclassMixin(A): # fails
#class CDataclassMixin(B, A): # works but fails type-check
c_field: int = 3
pass
class C(CDataclassMixin, B):
def method(self):
return "C's result"
pass
o = C(b_field=5)
Now, C has a_field and c_field but has lost b_field.
I have found that if I declare CDataclassMixin explicitly to inherit from B and A (in that order), b_field will be in the resulting class along with a_field_ and c_field`. However, explicitly stating the inheritance hierarchy in every mix-in defeats the purpose of mix-ins, which is to be able to code them independently of all the other mix-ins and to mix them easily and any way you like.
What is the correct way to make abstract dataclass mix-ins, so that classes that inherit from them include all the dataclass fields?
The correct solution is to abandon the DataclassMixin classes and simply make the abstract classes into dataclasses, like this:
#dataclass # type: ignore[misc]
class A(ABC):
a_field: int = 1
#abstractmethod
def method(self):
pass
#dataclass # type: ignore[misc]
class B(A):
b_field: int = 2
#dataclass
class C(B):
c_field: int = 3
def method(self):
return self
The reason for the failures is that, as explained in the documentation on dataclasses, the complete set of fields in a dataclass is determined when the dataclass is compiled, not when it is inherited from. The internal code that generates the dataclass's __init__ function can only examine the MRO of the dataclass as it is declared on its own, not when mixed in to another class.
It's necessary to add # type: ignore[misc] to each abstract dataclass's #dataclass line, not because the solution is wrong but because mypy is wrong. It is mypy, not Python, that requires dataclasses to be concrete. As explained by ilevkivskyi in mypy issue 5374, the problem is that mypy wants a dataclass to be a Type object and for every Type object to be capable of being instantiated. This is a known problem and awaits a resolution.
The behavior in the question and in the solution is exactly how dataclasses should behave. And, happily, abstract dataclasses that inherit this way (the ordinary way) can be mixed into other classes willy-nilly no differently than other mix-ins.
Putting the mixin as the last base class works without error:
#dataclass
class ADataclassMixin:
a_field: int = 1
class A(ABC, ADataclassMixin):
#abstractmethod
def method(self):
pass
#dataclass
class BDataclassMixin:
b_field: int = 2
class B(A, BDataclassMixin):
def method(self):
return self
o = B(a_field=5)
print((o.a_field, o.b_field)) # (5,2)
Related
I have a mixin class WithWorkMixin that has one abstract method def do_work(self) -> None.
I also have a metaclass to enforce singleton of child classes. Following is its definition.
from abc import ABCMeta
from typing import Any
from typing import Dict
class Singleton(ABCMeta):
_instances: Dict[type, Any] = {}
def __call__(cls: Any, *args: Any, **kwargs: Any) -> Any:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
Now, when I want to create a class A that inherits WithWorkMixin and uses the Singleton metaclass at the same time like the following,
class A(WithWorkMixin, metaclass=Singleton):
the abstract method in WithWorkMixin doesn't seem to be enforced. I can create a new instance named a of A without defining do_work. When I do type checking on that new instance, it looks like a is an instance of WithWorkMixin.
isinstance(a, WithWorkMixin) # return True
Could someone help me understand what's going on? Is there anything I can do to make it work? I guess I probably need to specify the inheritance within the Singleton class somehow. Thanks.
P.S. I did some additional tests. If I directly add an abstractmethod in A, it does give the error message as expected.
This is Python 3.10. My code is as follows:
from __future__ import annotations
from typing import Union
class Vehicle():
def __init__(self, components):
self.components = components
def getComponentWithFlag(self, flag: str) -> Union[Component,None]:
for component in self.components:
if getattr(component,flag,None):
return component
return None
class Component():
pass
class PassengerComponent(Component):
def __init__(self):
self.carriesPassengers = True
def ejectPassenger(self):
print('A passenger is tossed outside!')
class FreightComponent(Component):
def __init__(self):
self.carriesFreight = True
VW_Mini = Vehicle(components= [PassengerComponent()])
VW_Passat = Vehicle(components= [PassengerComponent(), FreightComponent()])
Truck = Vehicle(components= [FreightComponent()])
assert VW_Mini.getComponentWithFlag('carriesPassengers')
assert not VW_Mini.getComponentWithFlag('carriesFreight')
assert Truck.getComponentWithFlag('carriesFreight')
assert not Truck.getComponentWithFlag('carriesPassengers')
component = VW_Mini.getComponentWithFlag('carriesPassengers')
component.ejectPassenger()
Last line gives me a warning in PyCharm: Cannot find reference 'ejectPassenger' in 'Component | None'. I understand why it happens: there is no ejectPassenger method in Component class. Clearly the problem lies in how I typehint Vehicle.getComponentWithFlag method. Could you guys tell me how I should type hint its return object?
I know the object returned by that function:
may be None (if there's no appropriate Component),
may be an object of a subclass inheriting from Component class,
will never actually be an object of Component class itself.
Type hinting it explicitly like this: def getComponentWithFlag(self, flag: str) -> Union[PassengerComponent, FreightComponent, None] will not fly, because I will eventually have dozens of Components in my actual use-case and I would prefer to avoid typing them all out.
I would prefer to avoid typing them all out.
Unfortunately, I don't think it is possible, as you can't exclude root type only. (i.e. You can't hint all subclasses of T without T, as T is also subclass of T in static typing)
Instead, you can overload Vehicle.getComponentWithFlag.
from __future__ import annotations
from typing import Optional, Literal, Union, overload
class Vehicle():
def __init__(self, components):
self.components = components
#overload
def getComponentWithFlag(self, flag: Literal["carriesPassengers"]) -> Optional[PassengerComponent]:
...
#overload
def getComponentWithFlag(self, flag: Literal["carriesFreight"]) -> Optional[FreightComponent]:
...
def getComponentWithFlag(self, flag: str) -> Union[Component, None]:
for component in self.components:
if getattr(component,flag,None):
return component
return None
Try this code in your IDE. This code has limitation that you have to maintain overloaded variants as number of subclasses increases. However, I think this is optimal for now.
I'd like to type-annotate abstract class method witch behave as a constructor. For example in the code below, ElementBase.from_data is meant to be a abstract classmethod constructor.
tmp.py
from abc import abstractmethod, abstractclassmethod
import copy
from typing import TypeVar, Type
ElementT = TypeVar('ElementT', bound='ElementBase')
class ElementBase:
data: int
def __init__(self, data): self.data
##abstractmethod
def get_plus_one(self: ElementT) -> ElementT:
out = copy.deepcopy(self)
out.data = self.data + 1
return out
#abstractclassmethod
def from_data(cls: Type[ElementT], data: int) -> ElementT: # mypy error!!!
pass
class Concrete(ElementBase):
#classmethod
def from_data(cls, data: int) -> 'Concrete': # mypy error!!!
return cls(data)
However, applying mypy to this code shows the following erros.
tmp.py:18: error: The erased type of self "Type[tmp.ElementBase]" is not a supertype of its class "tmp.ElementBase"
tmp.py:23: error: Return type "Concrete" of "from_data" incompatible with return type <nothing> in supertype "ElementBase"
Do you have any idea to fix this error? Also, I'm specifically confused that the part of get_plus_one does not cause error, while only the part of abstractclassmethod does cause the error.
FYI, I want to make the abstract method constructor generic becaues I want to statically ensure that all subclass of ElementBase returns object with it's type when calling from_data.
[EDIT] comment out abstractmethod
It looks like mypy doesn't understand the abstractclassmethod decorator. That decorator has been deprecated since Python 3.3, as the abstractmethod and classmethod decorators were updated to play nice together. I think your code will work properly if you do:
#classmethod
#abstractmethod
def from_data(cls: Type[ElementT], data: int) -> ElementT:
pass
It's unrelated to your type checking issues, but you probably also want to change ElementBase to inherit from abc.ABC or to explicitly request the abc.ABCMeta metaclass if you want the abstractness of the class to be enforced by Python. Regular classes don't care about the abstractmethod decorator, and so as written, you'll be able to instantiate ElementBase (or you could if it's __init__ method didn't have an unrelated issue).
And another peripherally related note on this kind of type hinting... PEP 673 will add typing.Self in Python 3.11, which will be a convenient way for a method to refer to the type of object it's being called on. It should play nicely with classmethods without requiring you to jump through any hoops. With it you'd be able to write this much simpler version of the annotations:
#classmethod
#abstractmethod
def from_data(cls, data: int) -> Self:
pass
Trying to evaluate if dataclasses are suitable for an upcoming project, but right now I'm stuck with this code:
from dataclasses import dataclass
#dataclass
class MixinA:
attrA: int
def __post_init__(self):
print('MixinA post_init')
self.attrA = [self.attrA]
#dataclass
class MixinB:
attrB: str
def __post_init__(self):
print('MixinB post_init')
self.attrB = [self.attrB]
#dataclass
class MixinC:
attrC: bool
def __post_init__(self):
print('MixinC post_init')
self.attrC = [self.attrC]
#dataclass
class Inherited(MixinC, MixinB, MixinA):
pass
obj = Inherited(4, 'Hello', False)
print(obj.attrA, obj.attrB, obj.attrC)
print(obj.__class__.mro())
It is a surprise to me that only __post_init__() in the first base class is called, when I expect all three are invoked:
MixinC post_init
4 Hello [False]
[<class '__main__.Inherited'>, <class '__main__.MixinC'>, <class '__main__.MixinB'>, <class '__main__.MixinA'>, <class 'object'>]
Besides, changing inheritance doesn't do me any good. Following inheritance generates the exact same output as above:
class MixinA:
class MixinB(MixinA):
class MixinC(MixinB):
class Inherited(MixinC):
Did I write the testing code in a wrong way, or is current behavior done by oversight or intention?
The core issue for me is, I want to transform each attribute before generating the final dataclass instances. The actual inheritance is of larger scale, and doing it within each and every class would be very redundant.
If __post_init__() is a no-go, is there any alternative approach (such as InitVar or custom __init__())?
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.
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.