-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
pydantic.mypy rejects use of covariant type variable for frozen models #8607
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
Comments
The reason this happens is because:
You can see that this produces the same issue here: import dataclasses
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)
@dataclasses.dataclass
class Box(Generic[T_co]):
contents: T_co
@classmethod
def model_construct(cls, contents: T_co) -> 'Box[T_co]':
return cls(contents=contents) I am guessing that mypy just special cases to ignore this rule for I am not sure what to do about this, open to proposals. I can try to see if we can change the TypeVar used in the annotation in Some obvious, easier-to-implement things would be to provide a way to disable the creation of the I haven't looked into what's possible here so I will try to do that and provide an update here once I have a better sense of what we can do. |
Ah thank you that makes sense, it seems that python/mypy#6178 is exactly what is happening here, unfortunately it does not seem like it has any satisfying resolution. |
Thank you for the details. Thinking about it, I think mypy is right that model_construct is not valid for covariant types, since it uses cls which will have T_co bound to the value of the type variable for that instance if called on an instance. In the example of a Box container this will allow creating a Box[Derived] instance containing a Base instance, see the example below: from typing import TypeVar, Generic
from pydantic import BaseModel
T_co = TypeVar('T_co', covariant=True)
T = TypeVar('T')
class Box(BaseModel, Generic[T_co], frozen=True):
value: T_co
class Base(BaseModel, frozen<
8000
span class="pl-c1">=True):
pass
class Derived(Base, frozen=True):
pass
def replace(old: Box[Base], value: Base) -> Box[Base]:
return old.model_construct(value=value)
b = Box[Derived](value=Derived())
print(repr(b))
# Box[B](value=B())
a = replace(b, Base())
print(repr(a))
# Box[B](value=A()) This is exactly the same issue why a mutable container cannot be covariant. |
Thinking a bit more, the same issue crops up for class Box(Generic[T_co]):
def __init__(self, value: T_co) -> None:
self._value = value
@property
def value(self) -> T_co:
return self._value
class A:
pass
class B(A):
pass
class FancyBox(Box[B]):
def __init__(self, value: B) -> None:
super().__init__(value=value)
def construct(cls: type[Box[A]], value: A) -> Box[A]:
return cls(value=value)
val = construct(FancyBox, A()) |
Given your reflections in the above two comments, do you think it makes sense to close this issue, then? |
I still expect it to be valid to define immutable containers with covariant generic types. However I do see that the presence of alternative constructors on subtypes makes this unsound. But as my latter example shows, this is not only caused by model_construct since init exhibits similar behavior. For my usecase I would rather give up model_construct for the more natural modelling of our business logic that covariant containers allow. However I guess that means this turns out to be more of a feature request than a bug in the plugin as I initially thought. As for @dmontagu's guess that mypy special cases init this is confirmed by python/mypy#2850 where they just exclude init and new from the no covariant type variables as function arguments check. So I would prefer being able to disable the presence of model_construct for some model hierarchies. What do you think @dmontagu? |
Initial Checks
Description
Use of the pydantic.mypy plugin to mypy makes mypy complain about generic models with a field of a covariant type variable type. The error I get for the example code is:
Even if the error on the field declaration (for example the contents field on the Box model in the example) is ignored with # type: ignore then mypy gives an error on derived classes (for example Derived in the example) which pass on a covariant type variable to the parent.
Without the use of the pydantic.mypy plugin the code in the example is accepted both with frozen and non-frozen models.
I expect the use of a covariant type variable as a field type to be invalid if the model is not frozen but valid for a frozen model.
Example Code
Python, Pydantic & OS Version
The text was updated successfully, but these errors were encountered: