{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/python/statically-enforcing-frozen-dataclasses/",
"description": "Enforce immutable dataclasses at type-check time with @final decorator to catch mutations before runtime without frozen=True performance cost.",
"path": "/python/statically-enforcing-frozen-dataclasses/",
"publishedAt": "2024-01-04T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Python",
"TIL",
"Typing"
],
"textContent": "You can use @dataclass(frozen=True) to make instances of a data class immutable during\nruntime. However, there's a small caveat - instantiating a frozen data class is slightly\nslower than a non-frozen one. This is because, when you enable frozen=True, Python has to\ngenerate __setattr__ and __delattr__ methods during class definition time and invoke\nthem for each instantiation.\n\nBelow is a quick benchmark comparing the instantiation times of a mutable dataclass and a\nfrozen one (in Python 3.12):\n\nRunning this prints:\n\nSo, frozen data classes are approximately 2.4 times slower to instantiate than their\nnon-frozen counterparts. This gap can widen further if you compare slotted data classes (via\n@dataclass(slots=True)) with frozen ones. While the cost for immutability is small, it can\nadd up if you need to create many frozen instances.\n\nI was reading [Tin Tvrtković's article on zero-overhead frozen attrs] on making [attrs]\ninstances frozen at compile time. He mentions how to leverage mypy to enforce instance\nimmutability statically and use mutable attr classes at runtime to avoid any instantiation\ncost. I wanted to see if I could do the same with standard data classes.\n\nHere's how to do it:\n\nIt involves:\n\n- Using the type checker to ensure the data class instance is immutable.\n- Replacing the immutable data class with a more performant mutable one at runtime.\n\nThe if TYPE_CHECKING condition only executes during type-checking. In that block, we use\ntyping.dataclass_transform, introduced in [PEP-681], to create a construct similar to the\ndataclass function that type checkers recognize.\n\nThe frozen_default flag, added in Python 3.12, makes this work seamlessly via [PEP-681],\nbut the code should also function in Python 3.11 without changes, as dataclass_transform\naccepts any keyword arguments. In Python 3.10 and earlier, you can import\ndataclass_transform from typing_extensions and leave the rest of the code as is.\n\nThe else ... block is what runs when you actually execute the code. There, we're just\naliasing the vanilla dataclass function as frozen.\n\nRunning this code snippet results in:\n\nHowever, mypy will flag an error since we're trying to mutate foo.x:\n\nVoilà!\n\nI struggled to figure this one out myself, and LLMs were of no help. So, I ended up posting\na [question on Stack Overflow], where someone pointed out how to use dataclass_transform\nto achieve this.\n\nFin!\n\n\n\n\n[tin tvrtković's article on zero-overhead frozen attrs]:\n https://threeofwands.com/attra-iv-zero-overhead-frozen-attrs-classes/\n\n[attrs]:\n https://www.attrs.org/en/stable/\n\n[pep-681]:\n https://peps.python.org/pep-0681/\n\n[question on Stack Overflow]:\n https://stackoverflow.com/questions/77754655/how-to-statically-enforce-frozen-data-classes-in-python",
"title": "Statically enforcing frozen data classes in Python"
}