I have a couple of data structure classes that are intended to have public read-only attributes. I cannot use dataclasses or namedtuples as I need the ability to define args, kwargs in the child classes inheriting these classes.
One way I can set this up is simply having read-only properties but at some point, my data structure can have more than 20 attributes and writing a property for each one of them feels cumbersome.
I think the best solution for me would be to implement a descriptor and I have an implementation which works but mypy seems to be struggling with it:
from typing import Type, TypeVar, Generic
_Object = TypeVar("_Object")
_Value = TypeVar("_Value")
class ReadOnlyAttribute(Generic[_Value]):
def __set_name__(self, owner: Type[_Object], name: str) -> None:
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, instance: _Object, owner: Type[_Object]) -> _Value:
return getattr(instance, self.private_name)
def __set__(self, instance: _Object, value: _Value) -> None:
if hasattr(instance, self.private_name):
raise AttributeError(
f"Cannot set read-only attribute '{self.public_name}' of '{instance}'"
)
setattr(instance, self.private_name, value)
class Test:
# error: Incompatible types in assignment (expression has type "ReadOnlyAttribute[Never]", variable has type "str") [assignment]
foo: str = ReadOnlyAttribute()
def __init__(self):
self.foo = "foooooo"
# Revealed type is "builtins.str"
reveal_type(Test().foo)
It is able to infer the type correctly but is giving an error about incompatible types in assignment. I think it's because I am type-hinting self.foo as a str but assigning an instance of ReadOnlyAttribute?
However, without doing this and using Generic, reveal_type does not perform as expected and returns Never.
foo: str = ReadOnlyAttribute()- no, forgo thestrannotation and do this instead:foo = ReadOnlyAttribute[str](). But IMO a bigger usability issue is that you're not statically typing__set__to warn users of its read-only nature. I would dodef __set__(self, instance: typing_extensions.Never, value: <anything>) -> None: ...instead._Objecttypevar is used incorrectly in your snippet - it isn't bound to the class (probably good for usability) and only occurs in signatures of__set__and__set_name__once. I'd just use plainAnythere. You could makeReadOnlyAttributegeneric in this var, but that will add more verbosity for little to no benefit.Neverisn't correct, because__set__can (and will) be called once without raising. I think we'd need linear types to assert statically that__set__should only be called once.Test.__init__initialize the underlying attribute directly. Will your__set__actually apply any validation or logic to the value before initialization of the private attribute?__set__will be called once without raising when the value of the public attribute is set in the__init__as @chepner pointed out. However, even if I had to typehint as a never returning function, isn'tdef __set__(self, instance: _Object, value: <anything>): -> Never: ...better?