From cfb09936eb1971837dc4da74ae0401b10074900b Mon Sep 17 00:00:00 2001 From: Benitex Date: Mon, 8 Dec 2025 18:28:02 -0300 Subject: [PATCH 1/2] feat: error for Final fields with init=False (#13119) Test cases from PR #14285, thanks to @jakezych. Co-Authored-By: Jake Zych <56098501+jakezych@users.noreply.github.com> Co-Authored-By: Rafael Christof --- mypy/plugins/dataclasses.py | 8 ++++ test-data/unit/check-dataclasses.test | 56 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e916ded01dd2..188e7ece13f8 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -651,6 +651,14 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: elif not isinstance(stmt.rvalue, TempNode): has_default = True + if node.is_final and not is_in_init and not has_default: + has_post_init = cls.info.get("__post_init__") is not None + if not has_post_init: + self._api.fail( + f'Final field with init=False must have a default value', + 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 c7744e4a82a9..c42cb38a871c 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2737,3 +2737,59 @@ class ClassB(ClassA): def value(self) -> int: return 0 [builtins fixtures/dict.pyi] + +[case testErrorFinalFieldNoInitNoArgumentPassed] +from typing import Final +from dataclasses import dataclass, field +@dataclass +class Foo: + a: Final[int] = field(init=False) # E: Final field with init=False must have a default value +Foo().a +[builtins fixtures/dataclasses.pyi] + +[case testErrorFinalFieldInitNoArgumentPassed] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field() + +Foo().a # E: Missing positional argument "a" in call to "Foo" +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldGeneratedInitArgumentPassed] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field() + +Foo(1).a +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldPostInit] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field(init=False) + + def __post_init__(self): + self.a = 1 + +Foo().a +[builtins fixtures/dataclasses.pyi] + +[case testFinalFieldInitFalseWithDefault] +from typing import Final +from dataclasses import dataclass, field + +@dataclass +class Foo: + a: Final[int] = field(init=False, default=1) + +Foo().a +[builtins fixtures/dataclasses.pyi] From 2c9742b296f0eebb4af6f756313d89372831dfb3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:52:18 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/plugins/dataclasses.py | 3 +-- test-data/unit/check-dataclasses.test | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 188e7ece13f8..57c1ee410e8a 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -655,8 +655,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: has_post_init = cls.info.get("__post_init__") is not None if not has_post_init: self._api.fail( - f'Final field with init=False must have a default value', - stmt.rvalue + "Final field with init=False must have a default value", stmt.rvalue ) if not has_default and self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES: diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index c42cb38a871c..4bbccac78269 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2753,7 +2753,7 @@ from dataclasses import dataclass, field @dataclass class Foo: - a: Final[int] = field() + a: Final[int] = field() Foo().a # E: Missing positional argument "a" in call to "Foo" [builtins fixtures/dataclasses.pyi] @@ -2764,9 +2764,9 @@ from dataclasses import dataclass, field @dataclass class Foo: - a: Final[int] = field() + a: Final[int] = field() -Foo(1).a +Foo(1).a [builtins fixtures/dataclasses.pyi] [case testFinalFieldPostInit] @@ -2780,7 +2780,7 @@ class Foo: def __post_init__(self): self.a = 1 -Foo().a +Foo().a [builtins fixtures/dataclasses.pyi] [case testFinalFieldInitFalseWithDefault]