mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Protocol for an instance method taking a `self` argument

Open Kludex opened this issue 1 year ago • 3 comments

Bug Report

We have several protocols we use to define @model_validator in Pydantic V2, see here.

The problem is that the simplest use case fails. See below:

To Reproduce

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

Expected Behavior

I expect no issues with mypy. As I don't have any with pyright.

Actual Behavior

a.py:9: error: Argument 1 has incompatible type "Callable[[UserModel], UserModel]"; expected "ModelAfterValidator | ModelAfterValidatorWithoutInfo"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: mypy 1.4.1 (compiled: yes)
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): no config file provided.
  • Python version used: 3.11.1

Kludex avatar Jul 07 '23 09:07 Kludex

Here's a self contained example:

from typing import Any, Callable, Protocol, TypeVar

T = TypeVar('T')

class ModelAfterValidator(Protocol):
    @staticmethod
    def __call__(
        self: T,
    ) -> T:
        ...

def model_validator() -> Callable[[ModelAfterValidator], Any]:
    raise NotImplementedError


class UserModel:
    @model_validator()
    def check_passwords_match(self) -> 'UserModel':
        raise NotImplementedError

I guess the whole @staticmethod thing on a Protocol with self: <type> is to blame. Open to better suggestions.

adriangb avatar Jul 07 '23 16:07 adriangb

@Kludex can you update the title to something along the lines of "Protocol for an instance method taking a self argument"

adriangb avatar Jul 07 '23 16:07 adriangb

I think we can just use Callable for now, but is there any way to require a self parameter as the first argument and that the return type be the same type (which is what I was attempting to do originally)?

adriangb avatar Jul 10 '23 02:07 adriangb

I'm not able to repro the original bug in this bug report using mypy 1.5. Perhaps it was fixed in a recent release, or perhaps the pydantic library has changed since the original bug report.

erictraut avatar Aug 14 '23 22:08 erictraut

Pydantic has changed but the self contained example in https://github.com/python/mypy/issues/15620#issuecomment-1625688906 should still be valid

adriangb avatar Aug 14 '23 22:08 adriangb

I think mypy is correct to complain about https://github.com/python/mypy/issues/15620#issuecomment-1625688906 ; pyright does as well. check_passwords_match does not have the right return type in the presence of UserModel subclasses. If you change the annotation to def check_passwords_match(self: T) -> T:, both mypy and pyright accept it

hauntsaninja avatar Aug 15 '23 07:08 hauntsaninja

I guess then let me broaden the question: how can I type this decorator to enforce that it get applied to an instance method? In the real world model_validator takes a mode={before,after} parameter that uses overloading to satisfy two different signatures. The before case must be a class method and the after case must be an instance method. The classmethod isn't hard to enforce, it's instance methods that I'm struggling with. Making the first argument be self seemed like a reasonable compromise.

adriangb avatar Aug 15 '23 14:08 adriangb

I'm not sure exactly I understand, but in pydantic case I think you can try a self: BaseModel annotation

hauntsaninja avatar Aug 24 '23 08:08 hauntsaninja