I have a situation where I would like to be able to treat a frozen dataclass
instance as always having the latest data. Or in other words, I'd like to be able to detect if a dataclass instance has had replace
called on it and throw an exception. It should also only apply to that particular instance, so that creation/replacements of other dataclass instances of the same type do not affect each other.
Here is some sample code:
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class AlwaysFreshData:
fresh_data: str
def attempt_to_read_stale_data():
original = AlwaysFreshData(fresh_data="fresh")
unaffected = AlwaysFreshData(fresh_data="not affected")
print(original.fresh_data)
new = replace(original, fresh_data="even fresher")
print(original.fresh_data) # I want this to trigger an exception now
print(new.fresh_data)
The idea here is to prevent both accidental mutation and stale reads from our dataclass objects to prevent bugs.
Is it possible to to do this? Either through a base class or some other method?
EDIT: The intention here is to have a way of enforcing/verifying "ownership" semantics for dataclasses, even if it is only during runtime.
Here is a concrete example of a situation with regular dataclasses that is problematic.
@dataclass
class MutableData:
my_string: str
def sneaky_modify_data(data: MutableData) -> None:
some_side_effect(data)
data.my_string = "something else" # Sneaky string modification
x = MutableData(my_string="hello")
sneaky_modify_data(x)
assert x.my_string == "hello" # as a caller of 'sneaky_modify_data', I don't expect that x.my_string would have changed!
This can be prevented by using frozen dataclasses! But then there is still a situation that can lead to potential bugs, as demonstrated below.
@dataclass(frozen=True)
class FrozenData:
my_string: str
def modify_frozen_data(data: FrozenData) -> FrozenData:
some_side_effect(data)
return replace(data, my_string="something else")
x = FrozenData(my_string="hello")
y = modify_frozen_data(x)
some_other_function(x) # AHH! I probably wanted to use y here instead, since it was modified!
In summary, I want the ability to prevent sneaky or unknown modifications to data, while also forcing invalidation of data that has been replaced. This prevents the ability to accidentally use data that is out-of-date.
This situation might be familiar to some as being similar to the ownership semantics in something like Rust.
As for my specific situation, I already have a large amount of code that uses these semantics, except with NamedTuple
instances instead. This works, because modifying the _replace
function on any instance allows the ability to invalidate instances. This same strategy doesn't work as cleanly for dataclasses as dataclasses.replace
is not a function on the instances themselves.