From 519f79cb6139318034a7199e8e3c0c332ddc4a78 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 3 Oct 2026 18:34:41 +0100 Subject: [PATCH 1/5] Fix crash on invalid redefinition in dataclass --- mypy/plugins/dataclasses.py | 5 +++++ test-data/unit/check-dataclasses.test | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a511e714ac6b4..a45403192eee8 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -602,6 +602,11 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: assert isinstance(node, Var), node + if node is not lhs.node: + # There is an invalid redefinition. Avoid temptation to guess, + # and skip both, see testDataclassVariableBadRedefine. + continue + # x: ClassVar[int] is ignored by dataclasses. if node.is_classvar: continue diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index f43ac255373e6..65f8cb3232fd5 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -670,6 +670,31 @@ reveal_type(x) # N: Revealed type is "typing._SpecialForm" [builtins fixtures/dataclasses.pyi] [typing fixtures/typing-medium.pyi] +[case testFunctionAliasDataclassNoCrash] +from dataclasses import dataclass + +def fn(a: int) -> int: + return a + +@dataclass +class Test: + foo = fn + foo: int = 42 # E: Name "foo" already defined on line 8 +[builtins fixtures/dataclasses.pyi] +[typing fixtures/typing-medium.pyi] + +[case testDataclassVariableBadRedefine] +from dataclasses import dataclass + +@dataclass +class Test: + foo = "test" + foo: int = 42 # E: Name "foo" already defined on line 5 + +reveal_type(Test) # N: Revealed type is "def () -> __main__.Test" +[builtins fixtures/dataclasses.pyi] +[typing fixtures/typing-medium.pyi] + [case testDataclassOrdering] from dataclasses import dataclass From 45d0d849cf00a6247ea31567a4f77563c898b2b2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 3 Oct 2026 20:14:09 +0100 Subject: [PATCH 2/5] Make dataclasses consistent with regular classes --- mypy/plugins/dataclasses.py | 22 ----------- test-data/unit/check-dataclasses.test | 55 ++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a45403192eee8..56008ad4d6575 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -360,12 +360,10 @@ def transform(self) -> bool: if decorator_arguments["frozen"]: if any(not parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Frozen dataclass cannot inherit from a non-frozen dataclass", info) - self._propertize_callables(attributes, settable=False) self._freeze(attributes) else: if any(parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Non-frozen dataclass cannot inherit from a frozen dataclass", info) - self._propertize_callables(attributes) if decorator_arguments["slots"]: self.add_slots(info, attributes) @@ -780,26 +778,6 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None: var._fullname = info.fullname + "." + var.name info.names[var.name] = SymbolTableNode(MDEF, var) - def _propertize_callables( - self, attributes: list[DataclassAttribute], settable: bool = True - ) -> None: - """Converts all attributes with callable types to @property methods. - - This avoids the typechecker getting confused and thinking that - `my_dataclass_instance.callable_attr(foo)` is going to receive a - `self` argument (it is not). - - """ - info = self._cls.info - for attr in attributes: - if isinstance(get_proper_type(attr.type), CallableType): - var = attr.to_var(info) - var.info = info - var.is_property = True - var.is_settable_property = settable - var._fullname = info.fullname + "." + var.name - info.names[var.name] = SymbolTableNode(MDEF, var) - def _is_kw_only_type(self, node: Type | None) -> bool: """Checks if the type of the node is the KW_ONLY sentinel value.""" if node is None: diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 65f8cb3232fd5..d75b1547c13f4 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -695,6 +695,58 @@ reveal_type(Test) # N: Revealed type is "def () -> __main__.Test" [builtins fixtures/dataclasses.pyi] [typing fixtures/typing-medium.pyi] +[case testDataclassCallableAttrRegular] +from dataclasses import dataclass +from typing import Callable + +@dataclass +class Defer(Parent): + b: int + +@dataclass +class Parent: + key: Callable[[], str] + a: int + +reveal_type(Parent) # N: Revealed type is "def (key: def () -> builtins.str, a: builtins.int) -> __main__.Parent" +[builtins fixtures/dataclasses.pyi] +[typing fixtures/typing-medium.pyi] + +[case testDataclassCallableAttrDefault] +from dataclasses import dataclass +from typing import Callable + +@dataclass +class Defer(Parent): + b: int = 0 + +@dataclass +class Parent: + key: Callable[[], str] = lambda: "yes" + a: int = 0 + +reveal_type(Parent) # N: Revealed type is "def (key: def () -> builtins.str =, a: builtins.int =) -> __main__.Parent" +reveal_type(Parent.key) # N: Revealed type is "def () -> builtins.str" +[builtins fixtures/dataclasses.pyi] +[typing fixtures/typing-medium.pyi] + +[case testDataclassCallableAttrFrozen] +from dataclasses import dataclass +from typing import Callable + +@dataclass(frozen=True) +class Defer(Parent): + b: int + +@dataclass(frozen=True) +class Parent: + key: Callable[[], str] + a: int + +reveal_type(Parent) # N: Revealed type is "def (key: def () -> builtins.str, a: builtins.int) -> __main__.Parent" +[builtins fixtures/dataclasses.pyi] +[typing fixtures/typing-medium.pyi] + [case testDataclassOrdering] from dataclasses import dataclass @@ -1722,7 +1774,8 @@ class A: def func() -> None: pass -reveal_type(A.a) # N: Revealed type is "def (*Any, **Any)" +reveal_type(A.a) # E: Cannot access instance-only attribute "a" on class object \ + # N: Revealed type is "def (*Any, **Any)" A(a=func).a() A(a=func).a = func # E: Property "a" defined in "A" is read-only [builtins fixtures/dataclasses.pyi] From 05fbc10690b68b14aa5442d25bca0687f3896fe9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 3 Oct 2026 21:58:54 +0100 Subject: [PATCH 3/5] Keep some existing behavior --- mypy/plugins/dataclasses.py | 17 ++++++++++++++++ test-data/unit/check-dataclasses.test | 28 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 56008ad4d6575..c5aa89990a138 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -360,10 +360,12 @@ def transform(self) -> bool: if decorator_arguments["frozen"]: if any(not parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Frozen dataclass cannot inherit from a non-frozen dataclass", info) + self._expand_callables(attributes) self._freeze(attributes) else: if any(parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Non-frozen dataclass cannot inherit from a frozen dataclass", info) + self._expand_callables(attributes) if decorator_arguments["slots"]: self.add_slots(info, attributes) @@ -771,6 +773,9 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None: if var.is_final: continue # do not turn `Final` attrs to `@property` var.is_property = True + # Checker may get confused when getting a property with this flag, + # since normally it can be true only for real variables. + var.is_initialized_in_class = False else: var = attr.to_var(info) var.info = info @@ -778,6 +783,18 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None: var._fullname = info.fullname + "." + var.name info.names[var.name] = SymbolTableNode(MDEF, var) + def _expand_callables(self, attributes: list[DataclassAttribute]) -> None: + """Keep historical behavior for attributes with callable types.""" + info = self._cls.info + for attr in attributes: + if isinstance(get_proper_type(attr.type), CallableType): + sym_node = info.names.get(attr.name) + if sym_node is not None: + var = sym_node.node + if isinstance(var, Var): + # Expand types eagerly to support self-types. + var.type = attr.expand_type(info) + def _is_kw_only_type(self, node: Type | None) -> bool: """Checks if the type of the node is the KW_ONLY sentinel value.""" if node is None: diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index d75b1547c13f4..8ef282bb29218 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1780,6 +1780,34 @@ A(a=func).a() A(a=func).a = func # E: Property "a" defined in "A" is read-only [builtins fixtures/dataclasses.pyi] +[case testDataclassesCallableFrozenOverride] +from dataclasses import dataclass +from typing import Callable +@dataclass(frozen=True) +class A: + a: Callable[[int], str] = lambda x: "yes" + +def foo(x: int) -> str: + return "no" + +@dataclass(frozen=True) +class Sub(A): + # This is no-op for attribute access on instances, + # so we effectively check the subtyping as if accessed on class. + a = foo +[builtins fixtures/dataclasses.pyi] + +[case testDataclassesCallableFrozenSelf] +from dataclasses import dataclass +from typing import Self, Callable +@dataclass(frozen=True) +class A: + a: Callable[[Self], str] + +a: A +reveal_type(a.a) # N: Revealed type is "def (__main__.A) -> builtins.str" +[builtins fixtures/dataclasses.pyi] + [case testDataclassInFunctionDoesNotCrash] from dataclasses import dataclass From 23e20f6a60307e505fde3fdf91f65d867e9fc1d9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 3 Oct 2026 23:03:58 +0100 Subject: [PATCH 4/5] Just fix the crash --- mypy/plugins/dataclasses.py | 39 ++++++++++++++++----------- test-data/unit/check-dataclasses.test | 3 +-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index c5aa89990a138..731cc2c3533d1 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -360,12 +360,12 @@ def transform(self) -> bool: if decorator_arguments["frozen"]: if any(not parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Frozen dataclass cannot inherit from a non-frozen dataclass", info) - self._expand_callables(attributes) + self._propertize_callables(attributes, settable=False) self._freeze(attributes) else: if any(parent["frozen"] for parent in parent_decorator_arguments): self._api.fail("Non-frozen dataclass cannot inherit from a frozen dataclass", info) - self._expand_callables(attributes) + self._propertize_callables(attributes) if decorator_arguments["slots"]: self.add_slots(info, attributes) @@ -603,9 +603,11 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: assert isinstance(node, Var), node if node is not lhs.node: - # There is an invalid redefinition. Avoid temptation to guess, - # and skip both, see testDataclassVariableBadRedefine. - continue + # We create new Vars in _propertize_callables(), so it is OK. + if not isinstance(get_proper_type(node.type), CallableType): + # There is an invalid redefinition. Avoid temptation to guess, + # and skip both, see testDataclassVariableBadRedefine. + continue # x: ClassVar[int] is ignored by dataclasses. if node.is_classvar: @@ -773,9 +775,6 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None: if var.is_final: continue # do not turn `Final` attrs to `@property` var.is_property = True - # Checker may get confused when getting a property with this flag, - # since normally it can be true only for real variables. - var.is_initialized_in_class = False else: var = attr.to_var(info) var.info = info @@ -783,17 +782,25 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None: var._fullname = info.fullname + "." + var.name info.names[var.name] = SymbolTableNode(MDEF, var) - def _expand_callables(self, attributes: list[DataclassAttribute]) -> None: - """Keep historical behavior for attributes with callable types.""" + def _propertize_callables( + self, attributes: list[DataclassAttribute], settable: bool = True + ) -> None: + """Converts all attributes with callable types to @property methods. + + This avoids the typechecker getting confused and thinking that + `my_dataclass_instance.callable_attr(foo)` is going to receive a + `self` argument (it is not). + + """ info = self._cls.info for attr in attributes: if isinstance(get_proper_type(attr.type), CallableType): - sym_node = info.names.get(attr.name) - if sym_node is not None: - var = sym_node.node - if isinstance(var, Var): - # Expand types eagerly to support self-types. - var.type = attr.expand_type(info) + var = attr.to_var(info) + var.info = info + var.is_property = True + var.is_settable_property = settable + var._fullname = info.fullname + "." + var.name + info.names[var.name] = SymbolTableNode(MDEF, var) def _is_kw_only_type(self, node: Type | None) -> bool: """Checks if the type of the node is the KW_ONLY sentinel value.""" diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 8ef282bb29218..585ecfb9c893e 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1774,8 +1774,7 @@ class A: def func() -> None: pass -reveal_type(A.a) # E: Cannot access instance-only attribute "a" on class object \ - # N: Revealed type is "def (*Any, **Any)" +reveal_type(A.a) # N: Revealed type is "def (*Any, **Any)" A(a=func).a() A(a=func).a = func # E: Property "a" defined in "A" is read-only [builtins fixtures/dataclasses.pyi] From 6748926ab1704289d25d480610a9fbe269fc6a58 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 5 Oct 2026 13:58:42 +0100 Subject: [PATCH 5/5] Better tests --- test-data/unit/check-dataclasses.test | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 585ecfb9c893e..4fc2521941d29 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -680,6 +680,8 @@ def fn(a: int) -> int: class Test: foo = fn foo: int = 42 # E: Name "foo" already defined on line 8 + +reveal_type(Test.foo) # N: Revealed type is "def (a: builtins.int) -> builtins.int" [builtins fixtures/dataclasses.pyi] [typing fixtures/typing-medium.pyi] @@ -692,6 +694,7 @@ class Test: foo: int = 42 # E: Name "foo" already defined on line 5 reveal_type(Test) # N: Revealed type is "def () -> __main__.Test" +reveal_type(Test.foo) # N: Revealed type is "builtins.str" [builtins fixtures/dataclasses.pyi] [typing fixtures/typing-medium.pyi]