Making a Python data class frozen incurs a runtime performance penalty. There is of course an explanation for it, but the overall behaviour is a little counterintuitive – as a frozen data class can do less than its mutable counterpart.
Immutability is a very useful property in practical software applications. Especially when typecheckers like Pyright have started to rely on it to implement more accurate checks. [1]
I’d love to hear the communities feedback on two ideas:
Add a static-only configuration option to data classes to declare them as immutable. This would be understood by the type checker, but wouldn’t actually prevent any mutations at runtime. Similar to [this hack] to make attrs-backed objects “immutable” without a runtime penalty.
Make it so that declaring a dataclass as frozen somehow does not incur a runtime penalty.
I understand that both options have significant drawbacks. Whatever the implementation of 1 is, it may certainly lead to confusion with the already existing frozen setting. Similarly, I imagine 2 would be a very significant runtime effort.
However, I wouldn’t underestimate the positive (and ultimately simplifying) effects of bringing immutability to more of the language. Curious what the folks here think of this!
[1] For an example, see changes made in Pyright’s 1.1.328 release. It’s no longer possible to narrow the type of a data class property when inheriting from the class, unless that data class is frozen. Otherwise, a reportIncompatibleVariableOverride type error is raised.
A third idea comes to my mind. Could Python have a more generic way to mark things as ReadOnly / Immutable?
Perhaps similar to Final (PEP 591)? If this was broader than data classes, the risk of confusion with existing frozen configuration would be lower (I imagine the two qualifiers could coexist easily) and there would be no need to work about the runtime implementation.
Here’s a version that should work for all type checkers.
from typing import dataclass_transform, TYPE_CHECKING
if TYPE_CHECKING:
@dataclass_transform(frozen_default=True)
def static_frozen_dataclass(__cls): ...
else:
from dataclasses import dataclass as static_frozen_dataclass
@static_frozen_dataclass
class X:
x: int
x = X(0)
x.x = 5