pydantic: [PYD-136] ModelAfterValidator causing warnings from Pylance and mypy

Initial Checks

  • I confirm that I’m using Pydantic V2 installed directly from the main branch, or equivalent

Description

I want to use an ModelAvterValidator via the model_validator decorator, however, both Pylance and mypy are issuing warnings when doing so. To make sure I was using it correctly, I tried using the example code from the documentation and I am experiencing the same issues there. The code below results in the following two warnings:

Argument 1 has incompatible type "Callable[[UserModel, UserModel], Any]"; expected "Union[ModelAfterValidator, ModelAfterValidatorWithoutInfo]"  [arg-type] (mypy)

Argument of type "(cls: Self@UserModel, m: UserModel) -> UserModel" cannot be assigned to parameter of type "_AnyModeAfterValidator"
  Type "(cls: Self@UserModel, m: UserModel) -> UserModel" cannot be assigned to type "_AnyModeAfterValidator"
    Type "(cls: Self@UserModel, m: UserModel) -> UserModel" cannot be assigned to type "(self: _ModelType@__call__, __info: ValidationInfo) -> _ModelType@__call__"
      Parameter name mismatch: "self" versus "cls"
      Parameter 2: type "ValidationInfo" cannot be assigned to type "UserModel"
        "ValidationInfo" is incompatible with "UserModel"
    Type "(cls: Self@UserModel, m: UserModel) -> UserModel" cannot be assigned to type "(self: _ModelType@__call__) -> _ModelType@__call__"
      Parameter name mismatch: "self" versus "cls" (pylance)

When changing the parameter name cls to self as suggested by Pylance, the corresponding lines in the latter warning disappear, but nothing else happens and I am not sure whether that is what I am supposed to do there.

As my IDE I use VS Code OSS with the python, pylance, and mypy extensions, among others.

Example Code

from pydantic import BaseModel, model_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @model_validator(mode="after")
    def check_passwords_match(cls, m: "UserModel"):
        pw1 = m.password1
        pw2 = m.password2
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError("passwords do not match")
        return m

Python, Pydantic & OS Version

pydantic version: 2.0b3
pydantic-core version: 0.39.0 release build profile
install path: <path-to-workspace>/env/lib/python3.11/site-packages/pydantic
python version: 3.11.3 (main, Jun  5 2023, 09:32:32) [GCC 13.1.1 20230429]
platform: Linux-6.3.9-arch1-1-x86_64-with-glibc2.37
optional deps. installed: ['typing-extensions']

Selected Assignee: @hramezani

Selected Assignee: @Kludex

PYD-136

Selected Assignee: @lig

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 4
  • Comments: 18 (13 by maintainers)

Most upvoted comments

+1, played around with this for a bit but can’t get any model validators to work with mypy on V2

Yes please open a separate issue, thanks!

My bad, looks like my mypy cache was playing up, sorry.

Tested in a clean repo and seems to be working!

It seems like this works with pyright but not mypy. @Kludex if you didn’t already try the above, can you look into why it works with pyright but not mypy? It might be worth asking on the mypy issue tracker.

Can you use this instead?

from pydantic import BaseModel, model_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @model_validator(mode="after")
    def check_passwords_match(self) -> "UserModel":
        if self.password1 != self.password2:
            raise ValueError("passwords do not match")
        return self

@Kludex even with the @classmethod decorator, mypy is still unhappy:

from pydantic import BaseModel, model_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @model_validator(mode='after')
    @classmethod
    def check_passwords_match(cls, m: 'UserModel'):
        pw1 = m.password1
        pw2 = m.password2
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return m
error: Argument 1 has incompatible type "Callable[[type[UserModel], UserModel], Any]"; expected "ModelAfterValidator | ModelAfterValidatorWithoutInfo"  [arg-type]

(example taken from docs, mypy 1.4.1)

Looking at the excepted signatures:

https://github.com/pydantic/pydantic/blob/e0c0fe8cd6b6a1a5fdd43ab92d9814b278e324ad/pydantic/functional_validators.py#L402-L422

They doesn’t seem to match the one used in the example (for reference, the callback protocols defined for wrap and before seem to be working, and they don’t have the @staticmethod decorator)

Thank you, it seems I missed this documentation then.

You can add the decorator @classmethod, and the IDE will be happy. 🙏

We’ve already documented this behavior.