2

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.

16
  • 2
    foo: str = ReadOnlyAttribute() - no, forgo the str annotation 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 do def __set__(self, instance: typing_extensions.Never, value: <anything>) -> None: ... instead. Commented Sep 12, 2024 at 20:05
  • To add to previous comment, _Object typevar 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 plain Any there. You could make ReadOnlyAttribute generic in this var, but that will add more verbosity for little to no benefit. Commented Sep 12, 2024 at 20:36
  • 1
    Never isn'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. Commented Sep 12, 2024 at 22:23
  • You could implement these using properties, simply by allowing 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? Commented Sep 12, 2024 at 22:33
  • @dROOOze, __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't def __set__(self, instance: _Object, value: <anything>): -> Never: ... better? Commented Sep 13, 2024 at 4:15

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.