dacite icon indicating copy to clipboard operation
dacite copied to clipboard

from_dict resets `dataclasses.field` with argument `init=False` and `default_factory`

Open tzah4748 opened this issue 2 years ago • 1 comments

Describe the bug When one of the dataclass's fields is a field(init=False, default_factory=list/dict/whatever) Using from_dict to load an instance of the dataclass results in the field being overridden to the default_factory value effectively ignoring any modifications done in the __post_init__ method of the class.

To Reproduce

from dataclasses import dataclass, field
from dacite import from_dict

@dataclass
class A:
    x: str
    y: int
    z: list = field(init=False, default_factory=list)

    def __post_init__(self):
        self.z = [1,2,3]

data = {
    'x': 'test',
    'y': 1,
}

print(from_dict(data_class=A, data=data))  # Will print: A(x='test', y=1, z=[])
print(A(x='test', y=1))  # Will print: A(x='test', y=1, z=[1, 2, 3])
print(A(**data))  # Will print: A(x='test', y=1, z=[1, 2, 3])
from_dict(data_class=A, data=data) == A(**data)  # False

Expected behavior

from_dict(data_class=A, data=data) == A(**data)  # True

Environment

  • Python version: 3.10
  • dacite version: 1.8.1

tzah4748 avatar Sep 26 '23 14:09 tzah4748

Update: The problem originates from dacite/dataclasses.py

def create_instance(data_class: Type[T], init_values: Data, post_init_values: Data) -> T:
    instance = data_class(**init_values)
    for key, value in post_init_values.items():
        setattr(instance, key, value)
    return instance

Why would you need the post_init_values and why would you need to set these attributes ?

If a field is init=True it's value will be assigned in instance creation explicitly or by the defined field's default/default_factory If a field is init=False it is expected to:

  1. Have a default/default_factory assigned.
  2. Be assigned to the instance in the __post_init__ method.

In all possible cases, you shouldn't override the value using that for loop on the _post_init_values.items()

For the very least, if this is really needed for some reason (unknown to me), you could easily fix this by adding a call to the instance's __post_init__ method, if defined.

def create_instance(data_class: Type[T], init_values: Data, post_init_values: Data) -> T:
    instance = data_class(**init_values)
    for key, value in post_init_values.items():
        setattr(instance, key, value)
    if hasattr(instance, "__post_init__"):
        instance.__post_init__()
    return instance

tzah4748 avatar Jan 30 '24 08:01 tzah4748