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.