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.