Frozen Dataclass and inheritance

I try to figure out what’s happening when a frozen dataclass is inherited.

Let’s create a frozen dataclass:

from dataclasses import dataclass

@dataclass(frozen=True)
class Base:
    a: int = 0
    b: int = 0

base = Base()

assert base.a == 0
assert base.b == 0

I cannot modify or create an attribute. The following will raise a dataclasses.FrozenInstanceError:

base.a = 1
base.c = 1

That is the expected behaviour of a frozen dataclass.

Now, let’s inherit this class:

class Child(Base):
    c: int = 0

child = Child()
assert child.a == 0
assert child.b == 0
assert child.c == 0

I still cannot modify an attribute. This still fails:

child.a = 1

But I can create one!

child.d = 3
assert child.d == 3

If I want Child to behave like Base, I have to decorate it again. This fails as expected:

@dataclass(frozen=True)
class RestrictedChild(Base):
    c: int = 0

restricted_child = RestrictedChild()
restricted_child.d = 4

What’s confusing is the mixed behaviour of child.

Further note, child params say it is frozen: child.__dataclass_params__=_DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=True)

dataclasses tries it’s best to only affect classes it is directly applied to, which in this case means only freezing attributes that are directly part of Base. Neither child.c nor child.any_other_attribute is being write protected.

1 Like

Child.c is created for Child, thus it works, whilst Child.a and Child.b are inherited from Base. Bases __setattr__ method is most likely hard coded for a and b then (unless Child were to redefine them) whilst c is not hard coded.

from dataclasses import dataclass

@dataclass(frozen=True)
class Base:
    a: int = 0
    b: int = 0

class Child(Base):
    b: int = 0
    c: int = 0

child = Child()
child.a = 1 # Error, inherited from Base
child.b = 1 # Defined in Child
child.c = 1 # Same here

The issue with child.__dataclass_params__ most likely stems from inheritance of Base being broken in that way (I could imagine it is a bug).

Tbh thought, I could be wrong here.

Since you didn’t decorate Child with dataclass, c is a class attribute, not an instance attribute. a and b use __setattr__ generated by the parent dataclass and that’s why you see the error. It is not the case for the class attribute c (i.e. you can change it by Child.c = value) or any other instance attribute you later add (this applies to assignments like child.c = value as well – you are creating a new instance attribute that shadows the class atrribute).

This is intentional behaviour for dataclass inheritance. Classes inheriting from a frozen dataclass are not automatically frozen.

Here’s the code that generates the __setattr__ and __delattr__ methods for frozen dataclasses:

If you look at the generated methods you can see that there’s a check for type(self) is cls which is True for your Base and RestrictedChild classes as they are decorated with @dataclass(frozen=True).

For the Child class, ‘a’ and ‘b’ are inherited fields, so are protected by the or name in ... condition, but as the method was only generated for Base the first condition is False and you can still create, delete and modify ‘new’ attributes.

Thank you @all, it’s clearer now.