python / mypy

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

Overloading an abstract attribute with a Final one #14863

Open ydirson opened 1 year ago

ydirson commented 1 year ago

In the following code it would be useful to mark toto as final to make sure no code modifies it by error. However this triggers Cannot override writable attribute "toto" with a final one

from typing import Final

class Base:
    toto: str = NotImplemented
    def access_toto(self) -> str:
        return self.toto

class CmdA(Base):
    toto: Final[str] = "toto"

In fact I tend to agree with that error, in that what I'd really like to do is to annotate toto as "abstract final", which is not possible either as the use of toto: Final[str] triggers Final name must be initialized with a value, and toto: Final[str] = NotImplemented still gets the Cannot override final attribute "toto" (previously declared in base class "Base") the previous attempt also showed.

ikonst commented 1 year ago

If it's final, it means it cannot be changed

class Base:
    toto: str = NotImplemented

    def change_toto(self) -> None:
        self.toto = "spam"

class CmdA(Base):
    toto: Final[str] = "toto"

 c = CmdA()
 c.change_toto()  # changes `c.toto` even though it's final :(

What are you trying to achieve with Final?

ydirson commented 1 year ago

@ikonst you're right, base.toto should not be mutable, this was not the best of those attempts :) What I'd like is to ensure all concrete subclasses of Base have final strings for their .toto.

A5rocks commented 1 year ago

This doesn't require all subclasses to have final strings (I'm not sure how you could ensure that) but you can make your original example work with a readonly property!

from typing import Final
from abc import ABC, abstractmethod

class Base(ABC):
    @property
    @abstractmethod
    def toto(self) -> str:
        ...

    def access_toto(self) -> str:
        return self.toto

class CmdA(Base):
    toto: Final[str] = "toto"
ikonst commented 1 year ago

Should we close this, or do we consider this a usability problem with the current error "Cannot override writable attribute "{name}" with a final one'"?

For example, do we need treatment similar to this? https://github.com/python/mypy/blob/267d37685a35e25eb985d56b6c6881ba574fcc7f/mypy/messages.py#L1194-L1205

ydirson commented 1 year ago

My original point was more about:

  1. specifying an attribute in parent class without implying it would be writable, so that a derived class could refine it as being final
  2. going one step further and specifying that this attribute should not be writable

@A5rocks yes this answers point 1, though I prefer avoiding ABC when stuff like ABC.register() is not needed (it's a PITA when you start to have other metaclasses), so I prefer this slightly modified version:

from typing import Final

class Base:
    @property
    def toto(self) -> str:
        raise NotImplementedError()

    def access_toto(self) -> str:
        return self.toto

class CmdA(Base):
    toto: Final[str] = "toto"

For point 2 I still don't have any idea. I'd think the final annotation in superclass without an assignment could have a use here, it would just declare an "abstract attribute" -- assigning NotImplemented could likely not be interpreted in another way, but going that way creates a special case which the first option avoids. Would there be any problem going that way ?

@ikonst: yes I'd tend to think similar treatment would help, but it's not the whole of this ticket ;)

A5rocks commented 1 year ago

Hm I just re-read this and I think typing-wise a property setter that has a NoReturn return type might work for point 2, but I'm not sure if mypy supports that and I'm on mobile so can't test.