python / mypy

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

Strange error when declaring `@final` property #15325

Open tmke8 opened 1 year ago

tmke8 commented 1 year ago

Bug Report

Consider this code:

from typing import final

class A:
    def __init__(self):
        self._x = 4

    @property
    @final  # E: @final should be applied only to overload implementation
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value) -> None:
        self._x = value

class B(A):

    @property  # E: Cannot override final attribute "x" (previously declared in base class "A")
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value) -> None:
        self._x = value

https://mypy-play.net/?mypy=latest&python=3.11&gist=a4a1981a3db4ac546b9947bb344a5fd5

Mypy complains that @final is not applied to the "overload implementation", but the property is still successfully declared final.

I tried moving the @final decorator to the setter – in that case, mypy's first error goes away, but then also the second error vanishes, meaning that x wasn't actually marked as final.

(I also tried moving @final above @property but that produces the same result.)

Environment: See mypy-play link above.

sobolevn commented 1 year ago

So, you want to be able to have separate final properties and their setters? Am I correct?

tmke8 commented 1 year ago

Hmm... I wanted to declare the implementation of the settable property final – meaning that subclasses shouldn't be allowed to override the getter or the setter. Do you think this doesn't make sense because Final attributes aren't settable?

EDIT: so, to clarify, I wanted to make getter and setter both final, such that subclassses can override neither, but just making the getter final would also help, I guess

Prometheus3375 commented 1 year ago

It will be interesting to make getters final, but allow overwrite setters/getters. Or more generally, to make only a subset of (getter, setter, deleter) final.

Usually, overwriting a subset of property accessors in a subclass is done via referencing to superclass property. But this is not currently allowed by mypy:

class A:
    @property
    def a(self) -> int: return 1

class B(A):
    @A.a.setter # error: "Callable[[A], int]" has no attribute "setter"  [attr-defined]
    def a(self, value): pass
class A:
    @property
    def a(self) -> int: return 1

    @a.setter
    def a(self, value): pass

    @a.deleter
    def a(self): pass

class B(A):
    @A.a.setter # error: overloaded function has no attribute "setter"  [attr-defined]
    def a(self, value): pass

Returning to completely final properties. When a function name is repeated, mypy considers that it is being overloaded. In the example of @tmke8 mypy emits error at final, because x is redefined later.

I tried to bypass this error via different names for setter and deleter, but ended up with another error:

from typing import final

class A:
    @property
    @final
    def a(self) -> int: return 1

    @a.setter  # error: "Callable[[A], int]" has no attribute "setter"  [attr-defined]
    def a_setter(self, value): pass

    @a_setter.deleter
    def a_deleter(self): pass

    a = a_deleter
    del a_deleter, a_setter

class B(A):
    @property  # error: Cannot override final attribute "a" (previously declared in base class "A")  [misc]
    def a(self) -> int: return 2

    @a.setter
    def a(self, value): pass

Then I tried to use Final, but this made mypy to stop considering a as property.

from typing import final, Final, reveal_type

class A:
    def a_get(self) -> int: return 1

    def a_set(self, value): pass

    def a_del(self): pass

    a: Final[property] = property(a_get, a_set, a_del)
    del a_get, a_set, a_del

obj = A()
reveal_type(obj.a)  # note: Revealed type is "Any"
a.a = 1  # error: Name "a" is not defined  [name-defined]

class B(A):
    @property  # error: Cannot override final attribute "a" (previously declared in base class "A")  [misc]
    # error: Signature of "a" incompatible with supertype "A"  [override]
    def a(self) -> int: return 2

    @a.setter
    def a(self, value): pass

All in all, final poorly works with properties:

  1. Adding final after property to settable/deletable property emits error that final is used on "overloaded" version of function.
  2. Adding final after setter or deleter does not produce errors if property got overwritten in a subclass.

Also my examples shows that properties themselves are poorly supported:

  1. Taking property from superclass to overwrite/add only a part of accessors emits an error.
  2. Using another name for setter/deleter (this also defines another property) emits an error.
  3. Properties set via assignment are not considered as properties. Making properties via assignment is useful when you want to make only settable properties. I assume to be able to consider them correctly, properties should be generic [GetReturnType, SetValueType].