Repository: pydantic/pydantic

Issue:
[PYD-136] ModelAfterValidator causing warnings from Pylance and mypy
### Initial Checks

- [X] 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

```Python
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

```Text
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

<sub>[PYD-136](https://linear.app/pydantic/issue/PYD-136/modelaftervalidator-causing-warnings-from-pylance-and-mypy)</sub>

Selected Assignee: @lig


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

We've already documented this behavior.
Thank you, it seems I missed this documentation then.
@Kludex even with the `@classmethod` decorator, `mypy` is still unhappy:

```python
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
```

```sh
error: Argument 1 has incompatible type "Callable[[type[UserModel], UserModel], Any]"; expected "ModelAfterValidator | ModelAfterValidatorWithoutInfo"  [arg-type]
```

(example taken from [docs](https://docs.pydantic.dev/dev-v2/usage/validators/#model-validators), `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)
Right. My bad. I'm going to check properly.
Can you use this instead?

```py
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
```
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?
> 
> ```python
> 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
> ```

no warning with pyright.
+1, played around with this for a bit but can't get any model validators to work with mypy on V2
+1, @adriangb your example fails for me with mypy '1.4.0' with 

```
Argument 1 has incompatible type "Callable[[type[UserModel]], UserModel]"; expected "ModelAfterValidator | ModelAfterValidatorWithoutInfo"  [arg-type]
```

Note only `mode="after"` i've found problematic (haven't found a form that doesn't anger mypy), 

`mode="before"` seems to work perfectly :1st_place_medal: 
Reported on mypy for now: https://github.com/python/mypy/issues/15620

Resolving patch:
diff --git a/pydantic/functional_validators.py b/pydantic/functional_validators.py
--- a/pydantic/functional_validators.py
+++ b/pydantic/functional_validators.py
@@ -409,32 +409,17 @@ def __call__(  # noqa: D102
         ...
 
 
-class ModelAfterValidatorWithoutInfo(Protocol):
-    """A `@model_validator` decorated function signature. This is used when `mode='after'` and the function does not
-    have info argument.
-    """
-
-    @staticmethod
-    def __call__(  # noqa: D102
-        self: _ModelType,  # type: ignore
-    ) -> _ModelType:
-        ...
-
-
-class ModelAfterValidator(Protocol):
-    """A `@model_validator` decorated function signature. This is used when `mode='after'`."""
-
-    @staticmethod
-    def __call__(  # noqa: D102
-        self: _ModelType,  # type: ignore
-        __info: _core_schema.ValidationInfo,
-    ) -> _ModelType:
-        ...
+ModelAfterValidatorWithoutInfo = Callable[[_ModelType], _ModelType]
+"""A `@model_validator` decorated function signature. This is used when `mode='after'` and the function does not
+have info argument.
+"""
 
+ModelAfterValidator = Callable[[_ModelType, _core_schema.ValidationInfo], _ModelType]
+"""A `@model_validator` decorated function signature. This is used when `mode='after'`."""
 
 _AnyModelWrapValidator = Union[ModelWrapValidator, ModelWrapValidatorWithoutInfo]
 _AnyModeBeforeValidator = Union[ModelBeforeValidator, ModelBeforeValidatorWithoutInfo]
-_AnyModeAfterValidator = Union[ModelAfterValidator, ModelAfterValidatorWithoutInfo]
+_AnyModelAfterValidator = Union[ModelAfterValidator[_ModelType], ModelAfterValidatorWithoutInfo[_ModelType]]
 
 
 @overload
@@ -457,7 +442,9 @@ def model_validator(
 def model_validator(
     *,
     mode: Literal['after'],
-) -> Callable[[_AnyModeAfterValidator], _decorators.PydanticDescriptorProxy[_decorators.ModelValidatorDecoratorInfo]]:
+) -> Callable[
+    [_AnyModelAfterValidator[_ModelType]], _decorators.PydanticDescriptorProxy[_decorators.ModelValidatorDecoratorInfo]
+]:
     ...
 
 


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -1748,9 +1748,9 @@ class MyDataclass:
         b: float
 
         @model_validator(mode='after')
-        def double_b(cls, dc: 'MyDataclass'):
-            dc.b *= 2
-            return dc
+        def double_b(self) -> 'MyDataclass':
+            self.b *= 2
+            return self
 
     d = MyDataclass('1', b='2')
     assert d.a == 1
diff --git a/tests/test_generics.py b/tests/test_generics.py
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -94,7 +94,7 @@ class Result(BaseModel, Generic[data_type]):
 
 
 def test_value_validation():
-    T = TypeVar('T')
+    T = TypeVar('T', bound=Dict[Any, Any])
 
     class Response(BaseModel, Generic[T]):
         data: T
@@ -107,12 +107,11 @@ def validate_value_nonzero(cls, v: Any):
             return v
 
         @model_validator(mode='after')
-        @classmethod
-        def validate_sum(cls, m):
-            data = m.data
+        def validate_sum(self) -> 'Response[T]':
+            data = self.data
             if sum(data.values()) > 5:
                 raise ValueError('sum too large')
-            return m
+            return self
 
     assert Response[Dict[int, int]](data={1: '4'}).model_dump() == {'data': {1: 4}}
     with pytest.raises(ValidationError) as exc_info:
diff --git a/tests/test_root_model.py b/tests/test_root_model.py
--- a/tests/test_root_model.py
+++ b/tests/test_root_model.py
@@ -244,9 +244,9 @@ def words(cls, v):
 def test_model_validator_after():
     class Model(RootModel[int]):
         @model_validator(mode='after')
-        def double(cls, v):
-            v.root *= 2
-            return v
+        def double(self) -> 'Model':
+            self.root *= 2
+            return self
 
     assert Model('1').root == 2
     assert Model('21').root == 42
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1481,8 +1481,8 @@ def test_model_validator_returns_ignore():
     class Model(BaseModel):
         a: int = 1
 
-        @model_validator(mode='after')
-        def model_validator_return_none(cls, m):
+        @model_validator(mode='after')  # type: ignore
+        def model_validator_return_none(self) -> None:
             return None
 
     m = Model(a=2)
@@ -1727,9 +1727,9 @@ def pre_root(cls, values: Dict[str, Any]) -> Dict[str, Any]:
             return values
 
         @model_validator(mode='after')
-        def post_root(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+        def post_root(self) -> 'A':
             validate_stub('A', 'post')
-            return values
+            return self
 
     class B(A):
         @model_validator(mode='before')
@@ -1738,9 +1738,9 @@ def pre_root(cls, values: Dict[str, Any]) -> Dict[str, Any]:
             return values
 
         @model_validator(mode='after')
-        def post_root(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+        def post_root(self) -> 'B':
             validate_stub('B', 'post')
-            return values
+            return self
 
     A(x='pika')
     assert validate_stub.call_args_list == [[('A', 'pre'), {}], [('A', 'post'), {}]]
@@ -1858,9 +1858,9 @@ class Rectangle(BaseModel):
         model_config = ConfigDict(validate_assignment=True)
 
         @model_validator(mode='after')
-        def set_area(cls, m: 'Rectangle') -> 'Rectangle':
-            m.__dict__['area'] = m.width * m.height
-            return m
+        def set_area(self) -> 'Rectangle':
+            self.__dict__['area'] = self.width * self.height
+            return self
 
     r = Rectangle(width=1, height=1)
     assert r.area == 1
diff --git a/tests/test_validators_dataclass.py b/tests/test_validators_dataclass.py
--- a/tests/test_validators_dataclass.py
+++ b/tests/test_validators_dataclass.py
@@ -150,7 +150,7 @@ def add_to_a(cls, v):
 
 
 def test_model_validator():
-    root_val_values = []
+    root_val_values: list[Any] = []
 
     @dataclass
     class MyDataclass:
@@ -159,16 +159,16 @@ class MyDataclass:
 
         @field_validator('b')
         @classmethod
-        def repeat_b(cls, v):
+        def repeat_b(cls, v: str) -> str:
             return v * 2
 
         @model_validator(mode='after')
-        def root_validator(cls, m):
-            root_val_values.append(asdict(m))
-            if 'snap' in m.b:
+        def root_validator(self) -> 'MyDataclass':
+            root_val_values.append(asdict(self))
+            if 'snap' in self.b:
                 raise ValueError('foobar')
-            m.b = 'changed'
-            return m
+            self.b = 'changed'
+            return self
 
     assert asdict(MyDataclass(a='123', b='bar')) == {'a': 123, 'b': 'changed'}