strange dataclass decorator behaviour with fields

Viewed 227

I recently ran into strange behavior related to dataclasses in python. When using fields with default_factory, repeated call of the decorator results in an error.

As for me, this looks like a bug, because the field does not work out as a full-fledged default value. Accordingly, I would like to ask a question. what is the reason for this behavior of dataclasses?

source:

@dataclass
class A:
    a: int = 5
    b: int = 6
    
B = dataclass(A)
print(B == A)

output:

True

on the other hand

@dataclass
class A:
    a: int = 5
    b: List = field(default_factory=lambda: [1, 2, 3])
    
B = dataclass(A)
print(B == A)

output:

TypeError: non-default argument 'b' follows default argument

and finally if you change order of a and b in last snippet you'll obtain True in the output.

2 Answers

The error message:

TypeError: non-default argument 'b' follows default argument

is pretty clear, you can't put a fields without defaults following fields with them. Specifying a default_factory for a field is not the same as giving it a default value.

Luckily in this particular case there's an easy workaround, just define a default_factory for the a field too, so b no longer follows one with a default argument. I certainly don't understand all the details of the current dataclass implementation, so can't really say whether it's a bug or not — the comments in the code basically just echo what the error says.

That said, generally speaking default argument values shouldn't be mutable objects, and that's the primary use-case for using default_factory functions, so it's fairly obvious that initializing mutable fields types would need to be handled in a different manner than those with immutable ones, don't you think?

from dataclasses import dataclass, field
from typing import List

@dataclass
class A:
    a: int = field(default_factory=lambda: 5)
    b: List = field(default_factory=lambda: [1, 2, 3])

B = dataclass(A)
print(B == A)  # -> True

I was fooling around with this for a while, and came up with a pretty striaghtforward solution that looks like can work. As mentioned, this is no bug with dataclasses, because there's nothing that says it needs to support decorating the same class twice. Therefore I'm not entirely sure about what the issue is, however if for whatever reason we need to ensure a dataclass can support being decorated with the @dataclass decorator again, we need to ensure we unset all other values for a field that has a default_factory - other than the default_factory itself.

The below example demonstrates this pretty clearly:

from dataclasses import dataclass, field, fields, MISSING
from typing import List


@dataclass
class A:
    a: int = 5
    b: List = field(default_factory=lambda: [1, 2, 3])


print('== A Fields')
for f in fields(A):
    print(f)
    if f.default_factory is not MISSING:
        setattr(A, f.name, field(default_factory=f.default_factory))


B = dataclass(A)
print(B == A)
print()

print('== B Fields')
for f in fields(B):
    print(f)

Of course in your code, you can encapsulate the logic under print('== A Fields') into a helper function, and change A instead to a generic cls argument that gets passed in to a function. This would allow you to modify any dataclass fields that are set with a default_factory, so that no error is raised when the data class is re-decorated again.

Related