'How to make attribute in dataclass read-only?

Let's say I have a class like this:

class C:

    def __init__(self, stuff: int):
        self._stuff = stuff

    @property
    def stuff(self) -> int:
        return self._stuff

then stuff is read-only:

c = C(stuff=10)
print(c.stuff)  # prints 10

and

c.stuff = 2

fails as expected

AttributeError: can't set attribute

How can I get the identical behavior using a dataclass? If I wanted to also have a setter, I could do:

@dataclass
class DC:
    stuff: int
    _stuff: int = field(init=False, repr=False)

    @property
    def stuff(self) -> int:
        return self._stuff

    @stuff.setter
    def stuff(self, stuff: int):
        self._stuff = stuff

But how could I do it without the @stuff.setter part?



Solution 1:[1]

from dataclasses import dataclass

@dataclass(frozen=True)
class YourClass:
    """class definition"""

https://docs.python.org/3/library/dataclasses.html#frozen-instances

After instantiation of the class, when trying to change any of its properties, the exception is raised.

Solution 2:[2]

To get the boilerplate reduction that dataclass provides I found the only way to do this is with a descriptor.

In [236]: from dataclasses import dataclass, field
In [237]: class SetOnce:
     ...:     def __init__(self):
     ...:         self.block_set = False
     ...:     def __set_name__(self, owner, attr):
     ...:         self.owner = owner.__name__
     ...:         self.attr = attr
     ...:     def __get__(self, instance, owner):
     ...:         return getattr(instance, f"_{self.attr}")
     ...:     def __set__(self, instance, value):
     ...:         if not self.block_set:
     ...:             self.block_set = True
     ...:             setattr(instance, f"_{self.attr}", value)
     ...:         else:
     ...:             raise AttributeError(f"{self.owner}.{self.attr} cannot be set.")

In [239]: @dataclass
     ...: class Foo:
     ...:     bar:str = field(default=SetOnce())

In [240]: test = Foo("bar")

In [241]: test.bar
Out[241]: 'bar'

In [242]: test.bar = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-242-9cc7975cd08b> in <module>
----> 1 test.bar = 1

<ipython-input-237-bddce9441c9a> in __set__(self, instance, value)
     12             self.value = value
     13         else:
---> 14             raise AttributeError(f"{self.owner}.{self.attr} cannot be set.")
     15

AttributeError: Foo.bar cannot be set.

In [243]: test
Out[247]: Foo(bar='bar')

Solution 3:[3]

Because using the decorator in the class definition essentially triggers the @dataclass decorator to use the property object as a default field, it doesn't play nice. You can set the property outside like:

>>> from dataclasses import dataclass, field
>>> @dataclass
... class DC:
...     _stuff: int = field(repr=False)
...     stuff: int = field(init=False)
...
>>> DC.stuff = property(lambda self: self._stuff) # dataclass decorator cant see this
>>> dc = DC(42)
>>> dc
DC(stuff=42)
>>> dc.stuff
42
>>> dc.stuff = 99
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Solution 4:[4]

import operator

@dataclass
class Enum:
    name: str = property(operator.attrgetter("_name")) 

    def __init__(self, name):
        self._name = name

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michal VaĊĦut
Solution 2
Solution 3 juanpa.arrivillaga
Solution 4 shaurun