8000 pydantic.mypy rejects use of covariant type variable for frozen models · Issue #8607 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content

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

Open
1 task done
adament opened this issue Jan 23, 2024 · 7 comments
Open
1 task done
Assignees
Labels
bug V2 Bug related to Pydantic V2

Comments

@adament
Copy link
adament commented Jan 23, 2024

Initial Checks

  • I confirm that I'm using Pydantic V2

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:

test.py: error: Cannot use a covariant type variable as a parameter  [misc]
test.py:8: error: Cannot use a covariant type variable as a parameter  [misc]
Found 2 errors in 1 file (checked 1 source file)

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.

test.py: error: Cannot use a covariant type variable as a parameter  [misc]
Found 1 error in 1 file (checked 1 source file)

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

from pydantic import BaseModel
from typing import TypeVar, Generic

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


class Box(BaseModel, Generic[T_co], frozen=True):
    contents: T_co


T2_co = TypeVar("T2_co", covariant=True)


class Derived(Box[T2_co], Generic[T2_co], frozen=True):
    ...

Python, Pydantic & OS Version

pydantic version: 2.5.3
        pydantic-core version: 2.14.6
          pydantic-core build: profile=release pgo=false
                 install path: C:\Code\apps\micromamba\envs\pydantic-test\Lib\site-packages\pydantic
               python version: 3.11.7 | packaged by conda-forge | (main, Dec 23 2023, 14:27:59) [MSC v.1937 64 bit (AMD64)]
                     platform: Windows-10-10.0.19045-SP0
             related packages: mypy-1.8.0 typing_extensions-4.9.0
@adament adament added bug V2 Bug related to Pydantic V2 pending Is unconfirmed labels Jan 23, 2024
@sydney-runkle sydney-runkle removed the pending Is unconfirmed label Jan 24, 2024
@sydney-runkle
Copy link
Contributor

@adament,

Thanks for reporting this. @dmontagu is taking a closer look at this issue, and we'll get back to you with regards to a fix!

@dmontagu
Copy link
Contributor

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 __init__, but I'm not sure.

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 model_construct but I expect that could be very challenging (at least, I don't think we currently do anything like that and I'm not sure how hard it would be).

Some obvious, easier-to-implement things would be to provide a way to disable the creation of the model_construct method, or to just suggest using a # type: ignore for this. I am not sure if it's possible for the plugin to catch and suppress this error (if it is possible, it should).

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.

@adament
Copy link
Author
adament commented Jan 25, 2024

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.

@adament
Copy link
Author
adament commented Jan 26, 2024

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.

@adament
Copy link
Author
adament commented Jan 26, 2024

Thinking a bit more, the same issue crops up for __init__ and type[..] types. This again allows us to put a base value inside a container for derived instances.

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

@sydney-runkle
Copy link
Contributor

@adament,

Given your reflections in the above two comments, do you think it makes sense to close this issue, then?

@adament
Copy link
Author
adament commented Jan 28, 2024

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V2 Bug related to Pydantic V2
Projects
None yet
Development

No branches or pull requests

3 participants
0