Skip to content

fix: don't override __post_init__ for non-init fields absent from data - #301

Open
gaoflow wants to merge 1 commit into
konradhalas:masterfrom
gaoflow:fix/non-init-field-post-init-override
Open

fix: don't override __post_init__ for non-init fields absent from data#301
gaoflow wants to merge 1 commit into
konradhalas:masterfrom
gaoflow:fix/non-init-field-post-init-override

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 24, 2026

Copy link
Copy Markdown

Bug

When a dataclass field is declared field(init=False) with an explicit
default= or default_factory=, from_dict overwrites whatever
__post_init__ computed by calling setattr with the stored default
after construction — producing a result that differs from a direct
DataClass(**data) call.

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

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

data = {"x": "test"}
from_dict(A, data)    # A(x='test', items=[])   ← broken
A(**data)             # A(x='test', items=[1, 2, 3])  ← correct

Same bug with default=:

@dataclass
class B:
    name: str
    computed: str = field(init=False, default="")

    def __post_init__(self):
        self.computed = f"computed_{self.name}"

from_dict(B, {"name": "foo"})    # B(name='foo', computed='')    ← broken
B("foo")                          # B(name='foo', computed='computed_foo')  ← correct

Fixes #244, #285.

Root cause

_from_dict iterates over all dataclass fields. When a key is absent from the
input dict and the field has init=False, get_default_value_for_field returns
the default value; that value is then placed in post_init_values and applied
with setattr after __post_init__ has already run.

Fix

Add an early continue in the else branch for the absent-key case: when
field.init is False and the field carries an explicit default or
default_factory, skip it entirely and let __post_init__ manage the value.

Fields with init=False but no explicit default (e.g. Optional[str] that
relies on dacite's is_optional → None fallback) retain their existing
behaviour.

Tests

Two new test cases cover the default and default_factory variants; all 205
tests pass.

When a dataclass field is declared with `field(init=False)` and carries
an explicit `default=` or `default_factory=`, `from_dict` would
incorrectly push the stored default value into `post_init_values` and
then call `setattr` on the freshly constructed instance — overwriting
whatever `__post_init__` had computed from the other fields.

Root cause: the `else` branch (key absent from the input dict) called
`get_default_value_for_field`, stored the result in `post_init_values`,
and applied it after construction regardless of whether the field is
managed by `__post_init__`.

Fix: when the key is absent AND the field has `init=False` AND the field
carries an explicit `default` or `default_factory`, skip the field
entirely.  Fields with `init=False` but no explicit default (e.g.
`Optional[str]` that relies on dacite's `is_optional → None` fallback)
continue to receive the inferred default, preserving existing behaviour.

Fixes konradhalas#244, konradhalas#285
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant