diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 7a6a3876d9bfd..1325f193bd1f2 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -654,6 +654,18 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: elif not isinstance(stmt.rvalue, TempNode): has_default = True + if ( + node.is_final + and has_field_call + and not is_in_init + and not has_default + and not is_init_var + and self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES + ): + # A final attribute can only be assigned in __init__(), so if it is + # excluded from __init__() and has no default value, it can never be set. + self._api.fail("Final field must have a default value if init=False", stmt.rvalue) + if not has_default and self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES: # Make all non-default dataclass attributes implicit because they are de-facto # set on self in the generated __init__(), not in the class body. On the other diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index d269ad2bad0ec..0549f1d3e0a04 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2952,6 +2952,27 @@ class Bad2: x: InitVar[Final[int]] # E: Final can be only used as an outermost qualifier in a variable annotation [builtins fixtures/dict.pyi] +[case testDataclassFinalFieldNotInInit] +from dataclasses import dataclass, field +from typing import Final + +@dataclass +class Bad: + x: Final[int] = field(init=False) # E: Final field must have a default value if init=False + +@dataclass +class GoodDefault: + x: Final[int] = field(init=False, default=0) + +@dataclass +class GoodFactory: + x: Final[int] = field(init=False, default_factory=int) + +@dataclass +class GoodPlain: + x: Final[int] = 0 +[builtins fixtures/tuple.pyi] + [case testNoCrashOnNestedSpecialFormsWithAnnotations] import dataclasses from typing import ClassVar, NamedTuple