@dataclasses.dataclass
class Mutable_Foo:
ham: str
eggs: int
A frozen dataclass is easy:
@dataclasses.dataclass(frozen=True)
class Frozen_Foo:
ham: str
eggs: int
But I frequently want to use both Mutable_Foo and Frozen_Foo in the same codebase, with exactly the same fields, with the only difference being whether it’s frozen or not.
Obviously I can just define them both, but then I’m repeating the field list twice, and it’s easy for them to end up out of sync after changing something.
Is there an idiomatic way to define both Mutable_Foo and Frozen_Foo at once, without repeating the field list?
I’m basically hoping for something like:
@dataclasses.dataclass
class Mutable_Foo:
ham: str
eggs: int
Frozen_Foo = make_frozen_dataclass(Mutable_Foo)
It’s important that type checkers (specifically Pylance) can understand it and apply the same static analysis that they would if they were defined separately.
but assuming I do that, the real problem is, if I then try to do
@dataclasses.dataclass
class Another:
foo: Frozen_Foo
I get a “Variable not allowed in type expression” error from Pylance.
I also tried having Frozen_Foo and Mutable_Foo extend Base_Foo, as in inheritance, without decorating Base_Foo at all, but that just results in Frozen_Foo and Mutable_Foo having no parameters in their constructor, so doesn’t work either.
In [1]: from dataclasses import make_dataclass
In [2]: fields = [("ham", str), ("eggs", int)]
In [3]: MutFoo = make_dataclass("MutFoo", fields)
In [4]: m = MutFoo('a', 2)
In [5]: m
Out[5]: MutFoo(ham='a', eggs=2)
In [6]: m.eggs
Out[6]: 2
In [7]: m.eggs = 3
In [8]: m
Out[8]: MutFoo(ham='a', eggs=3)
In [9]: FrozenFoo = make_dataclass("FrozenFoo", fields, frozen=True)
In [10]: f = FrozenFoo('b', 2)
In [11]: f.eggs = 1
---------------------------------------------------------------------------
FrozenInstanceError Traceback (most recent call last)
Cell In[11], line 1
----> 1 f.eggs = 1
File <string>:4, in __setattr__(self, name, value)
FrozenInstanceError: cannot assign to field 'eggs'
I don’t know if it’s considered idiomatic but it’ll do what you want here. Although the typing issue might remain.
This doesn’t work, but not due to syntax. Type checkers might complain that the call is incorrect because it doesn’t match the signatures in typeshed, but it does work with the actual function. However this doesn’t function correctly because the dataclass decorator function actually modifies the original class.
In this case first the __init__ method is written with the first call to dataclass on Base_Foo. When the class is then reprocessed for Frozen_Foo the __setattr__ method is added which prevents attribute assignment but the original __init__ is not replaced. As the __init__ method was written for a non-frozen class it uses regular attribute assignment and fails when you try to create an instance.
Traceback (most recent call last):
File "/home/david/src/scratch/scratch.py", line 15, in <module>
Frozen_Foo("", 1)
~~~~~~~~~~^^^^^^^
File "<string>", line 3, in __init__
File "<string>", line 16, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'ham'
If you check at the end you can see that the classes Mutable_Foo, Frozen_Foo and Base_Foo are all the same object[1].
This doesn’t hold if you use slots=True, but even that still modifies the original class. ↩︎
It does; make_dataclass is not recognized as creating a static type. It’s the dataclass decorator that gets special-cased by type checkers to allow what is otherwise a dynamically generated type to be used statically.