Need a way to type-hint attributes that is compatible with duck-typing

One way to think about it is that these modifiers can be translated into knowledge about the classes __getattr__ and __setattr__ methods (*):

Note: I abbreviate Literal["foo"]"foo", otherwise the table gets too wide.

Modifier self.__getattr__ self.__setattr__
foo: Readable[T] (name: "foo") -> T
foo: ReadOnly[T] (name: "foo") -> T (name: "foo", val: Never) -> None
foo: Writeable[T] (name: "foo", val: T) -> None
foo: WriteOnly[T] (name: "foo") -> Never (name: "foo", val: T) -> None
foo: Mutable[T] (name: "foo") -> T (name: "foo", val: T) -> None

(*) If Never is interpreted as the true, uninhabitable bottom type (uninhabitable means that no instances can exist, i.e. calling __setattr__ with Literal["foo"] and T is equivalent to raising an exception). It has come to my knowledge that unfortunately Never is not considered uninhabitable by python’s type-checkers, so possibly there needs to be another PEP to introduce a true bottom type that is uninhabitable.

Example of applying these principles
class A:
    foo: Readable[int]
    bar: ReadOnly[bool]
    baz: Mutable[str]

From a type-theory POV, this should be translatable to

class A:
    @overload
    def __getattr__(self, name: Literal["foo"]) -> int: ...
    @overload
    def __getattr__(self, name: Literal["bar"]) -> bool: ...
    @overload
    def __getattr__(self, name: Literal["baz"]) -> str: ...


    @overload
    def __setattr__(self, name: Literal["bar"], value: bool) -> Never: ...
    @overload
    def __setattr__(self, name: Literal["baz"], value: str) -> None: ...

EDIT: For ReadOnly, the __setattr__ might actually be better represented by (name: "foo", val: Never) -> None than (name: "foo", val: T) -> Never. This still prevents calling obj.foo = ..., but at the same time allows contravariant overriding, so that a subclass could replace a ReadOnly variable with a Mutable variable.

4 Likes