[Python-Dev] Should the dataclass frozen property apply to subclasses?

Eric V. Smith eric at trueblade.com
Tue Feb 27 19:37:45 EST 2018


On 2/22/2018 1:56 AM, Raymond Hettinger wrote:
> When working on the docs for dataclasses, something unexpected came up.  If a dataclass is specified to be frozen, that characteristic is inherited by subclasses which prevents them from assigning additional attributes:
> 
>      >>> @dataclass(frozen=True)
>      class D:
>              x: int = 10
> 
>      >>> class S(D):
>              pass
> 
>      >>> s = S()
>      >>> s.cached = True
>      Traceback (most recent call last):
>        File "<pyshell#49>", line 1, in <module>
>          s.cached = True
>        File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/dataclasses.py", line 448, in _frozen_setattr
>          raise FrozenInstanceError(f'cannot assign to field {name!r}')
>      dataclasses.FrozenInstanceError: cannot assign to field 'cached'
> 
> Other immutable classes in Python don't behave the same way:
> 
> 
>      >>> class T(tuple):
>              pass
> 
>      >>> t = T([10, 20, 30])
>      >>> t.cached = True
> 
>      >>> class F(frozenset):
>              pass
> 
>      >>> f = F([10, 20, 30])
>      >>> f.cached = True
> 
>      >>> class B(bytes):
>              pass
> 
>      >>> b = B()
>      >>> b.cached = True
> 

I'll provide some background, then get in to the dataclasses design 
issues. Note that I'm using "field" here in the PEP 557 sense.

There are some questions to resolve:

1. What happens when a frozen dataclass inherits from a non-frozen 
dataclass?

2. What happens when a non-frozen dataclass inherits from a frozen 
dataclass?

3. What happens when a non-dataclass inherits from a frozen dataclass?

4. Can new non-field attributes be created for frozen dataclasses?


I think it's useful to look at what attrs does. Unsurprisingly, attrs 
works the way the dataclasses implementation in 3.7.0a1 works:

- If a frozen attrs class inherits from a non-frozen attrs class, the 
result is a frozen attrs class.

- If a non-frozen attrs class inherits from a frozen attrs class, the 
result is a frozen attrs class.

- For a frozen attrs class, you may not assign to any field, nor create 
new non-field instance attributes.

- If a non-attrs class derives from a frozen attrs class, then you 
cannot assign to or create any non-field instance attributes. This is 
because they override the class's __setattr__ to always raise. This is 
the case that Raymond initially brought up on this thread (but for 
dataclasses, of course).

As I said, this is also how 3.7.0a1 dataclasses also works. The only 
difference between this and 3.7.0.a2 is that I prohibited inheriting a 
non-frozen dataclass from a frozen one, and also prohibited the 
opposite: you can't inherit a frozen dataclass from a non-frozen 
dataclass. This was just a stop-gap measure to give us more wiggle room 
for future changes. But this does nothing to address Raymond's concern 
about non-dataclasses deriving from frozen dataclasses.

A last piece of background info on how dataclasses and attrs work: the 
most derived class implements all of the functionality. They never call 
in to the base class to do anything. The base classes just exist to 
provide the list of fields.

If frozen dataclasses only exist to protect fields that belong to the 
hash, then my suggestion is to change the implementation of frozen class 
to use properties instead of overwriting __setattr__ (Nick also 
suggested this). This would allow you to create non-field attributes. If 
the purpose is really to prevent any attributes from being added or 
modified, then I think __setattr__ should stay but we should change it 
to allow non-dataclass subclasses to add non-field instance attributes 
(addressing Raymond's concern).

I think we shouldn't allow non-field instance attributes to be added to 
a frozen dataclass, although if anyone has a strong feeling about it, 
I'd like to hear it.

So, given a frozen dataclass "C" with field names in "field_names", I 
propose changing __setattr__ to be:

def __setattr__(self, name, value):
     if type(self) is C or name in field_names:
         raise FrozenInstanceError(f'cannot assign to field {name!r}')
     super(cls, self).__setattr__(name, value)

In the current 3.7.0a2 implementation of frozen dataclasses, __setattr__ 
always raises. The change is the test and then call to 
super().__setattr__ if it's a derived class. The result is an exception 
if either self is an instance of C, or if self is an instance of a 
derived class, but the attribute being set is a field of C.

So going back to original questions above, my suggestions are:

1. What happens when a frozen dataclass inherits from a non-frozen 
dataclass? The result is a frozen dataclass, and all fields are 
non-writable. No non-fields can be added. This is a reversion to the 
3.7.0a1 behavior.

2. What happens when a non-frozen dataclass inherits from a frozen 
dataclass? The result is a frozen dataclass, and all fields are 
non-writable. No non-fields can be added. This is a reversion to the 
3.7.0a1 behavior. I'd also be okay with this case being an error, and 
you'd have to explicitly mark the derived class as frozen. This is the 
3.7.0a2 behavior.

3. What happens when a non-dataclass inherits from a frozen dataclass? 
The fields that are in the dataclass are non-writable, but new non-field 
attributes can be added. This is new behavior.

4. Can new non-field attributes be created for frozen dataclasses? No. 
This is existing behavior.


I'm hoping this change isn't so large that we can't get it in to 3.7.0a3 
next month.

Eric


More information about the Python-Dev mailing list