python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.35k stars 2.81k forks source link

signatures of setters are not checked #12892

Open DetachHead opened 2 years ago

DetachHead commented 2 years ago
class A:
    @property
    def foo(self) -> int:
        ...
    @foo.setter
    def foo(self, value: int, value2: str) -> None: # no mypy error
        ...

a = A()

a.foo = 1 # runtime error - TypeError: A.foo() missing 1 required positional argument: 'value2'

https://mypy-play.net/?mypy=latest&python=3.10&flags=show-error-context%2Cshow-error-codes%2Cstrict%2Ccheck-untyped-defs%2Cdisallow-any-decorated%2Cdisallow-any-expr%2Cdisallow-any-explicit%2Cdisallow-any-generics%2Cdisallow-any-unimported%2Cdisallow-incomplete-defs%2Cdisallow-subclassing-any%2Cdisallow-untyped-calls%2Cdisallow-untyped-decorators%2Cdisallow-untyped-defs%2Cno-implicit-optional%2Cno-implicit-reexport%2Clocal-partial-types%2Cstrict-equality%2Cwarn-incomplete-stub%2Cwarn-redundant-casts%2Cwarn-return-any%2Cwarn-unreachable%2Cwarn-unused-configs%2Cwarn-unused-ignores&gist=3a081cf4b94705271106cf081232887f

DetachHead commented 2 years ago

also no error when there's no second argument:

class A:
    @property
    def foo(self) -> int:
        ...
    @foo.setter
    def foo(self) -> None: # no mypy error
        ...

a = A()

a.foo = 1 # runtime error - TypeError: A.foo() takes 1 positional argument but 2 were given
DetachHead commented 2 years ago

and also no error when the setter type is different to the getter type

class Foo:
    @property
    def foo(self) -> int:
        ...

    @foo.setter
    def foo(self, value: str) -> None:
        ...

however you get an error when attempting to use the setter:

f = Foo()
f.foo = "" # Incompatible types in assignment (expression has type "str", variable has type "int")
q-wertz commented 1 year ago

Similar example:

class A:
    def init(self) -> None:
        self._a: int | None = None

    @property
    def a(self) -> int:
        if self._a is None:
            raise ValueError
        return self._a

    @a.setter
    def a(self, val: int | None) -> None:
        self._a = val

a = A()

a.a = None  # Incompatible types in assignment (expression has type "None", variable has type "int")  [assignment]

In my scenario I would like it to work like this but if it is bad practice that the setter allows different types than the getter it should warn in the definition of the setter…

ydirson commented 1 year ago

I would argue that it is not necessarily a problem in itself. We can use this to implement a "smart setter" that takes as type to build on this example Union[int, str], and tries to convert a str to an int.

What I see as a problem is rather that when you then try to set foo in a way compatible with the declaration and implementation (ie. assign a string to it), then despite the code being correct, mypy will ignore the type declared in the setter, and validate against the getter type instead - see https://github.com/python/mypy/issues/3004 where the consensus is that this is a valid use case.

Maybe close this ticket?

DetachHead commented 1 year ago

Even if #3004 gets implemented, there are still several examples here of invalid setters that should be errors (incorrect number of arguments) so I'm leaving this issue open regardless