This answer extends directly from my other post on using descriptor classes, which is a convenient and handy way to define properties, more or less.
Since dataclasses does not offer a field(frozen=True)
approach, I think this one can instead work for you.
Here is a straightforward example of usage below:
from dataclasses import dataclass, MISSING
from typing import Generic, TypeVar
_T = TypeVar('_T')
class Frozen(Generic[_T]):
__slots__ = (
'_default',
'_private_name',
)
def __init__(self, default: _T = MISSING):
self._default = default
def __set_name__(self, owner, name):
self._private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self._private_name, self._default)
return value
def __set__(self, obj, value):
if hasattr(obj, self._private_name):
msg = f'Attribute `{self._private_name[1:]}` is immutable!'
raise TypeError(msg) from None
setattr(obj, self._private_name, value)
@dataclass
class DC:
stuff: int = Frozen()
other_stuff: str = Frozen(default='test')
dc = DC(stuff=10)
# raises a TypeError: Attribute `stuff` is immutable!
# dc.stuff = 2
# raises a TypeError: Attribute `other_stuff` is immutable!
# dc.other_stuff = 'hello'
print(dc)
# raises a TypeError: __init__() missing 1 required positional argument: 'stuff'
# dc = DC()
Another option, is to use a metaclass which automatically applies the @dataclass
decorator. This has a few advantages, such as being able to use dataclasses.field(...)
for example to set a default value if desired, or to set repr=False
for instance.
Note that once @dataclass_transform
comes out in PY 3.11, this could potentially be a good use case to apply it here, so that it plays more nicely with IDEs in general.
In any case, here's a working example of this that I was able to put together:
from dataclasses import dataclass, field, fields
class Frozen:
__slots__ = ('private_name', )
def __init__(self, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
return value
def __set__(self, obj, value):
if hasattr(obj, self.private_name):
msg = f'Attribute `{self.private_name[1:]}` is immutable!'
raise TypeError(msg) from None
setattr(obj, self.private_name, value)
def frozen_field(**kwargs):
return field(**kwargs, metadata={'frozen': True})
def my_meta(name, bases, cls_dict):
cls = dataclass(type(name, bases, cls_dict))
for f in fields(cls):
# if a dataclass field is supposed to be frozen, then set
# the value to a descriptor object accordingly.
if 'frozen' in f.metadata:
setattr(cls, f.name, Frozen(f.name))
return cls
class DC(metaclass=my_meta):
other_stuff: str
stuff: int = frozen_field(default=2)
# DC.stuff = property(lambda self: self._stuff)
dc = DC(other_stuff='test')
print(dc)
# raises TypeError: Attribute `stuff` is immutable!
# dc.stuff = 41
dc.other_stuff = 'hello'
print(dc)