Idiomatic way to create a frozen + mutable dataclass pair?

A mutable dataclass is easy:

@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.

@Decorator syntax is just sugar, for a function call and assignment, so how about something like?:

class Base_Foo:
  ham: str
  eggs: int

Mutable_Foo = dataclasses.dataclass(Base_Foo)
Frozen_Foo = dataclasses.dataclass(Base_Foo, frozen=True)

If they can’t be inherited directly. Look up if dataclasses have a functional API too.

1 Like

It’s a nice suggestion, which I hadn’t thought of, but it doesn’t seem to work.

The required syntax is actually

Frozen_Foo = dataclasses.dataclass(frozen=True)(Base_Foo)

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.

I think what you’re looking for is dataclasses.make_dataclass, e.g.

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.

3 Likes

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].


  1. This doesn’t hold if you use slots=True, but even that still modifies the original class. ↩︎

the dataclass decorator function actually modifies the original class

Ah, I see. Thanks.

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.