From 2462afa8366f2c1bc42fd8982a7b48a113b72e3b Mon Sep 17 00:00:00 2001 From: Max Rekunchak Date: Sun, 12 Jul 2026 02:55:06 +0300 Subject: [PATCH] gh-148941: Skip generating __init__, __repr__, __eq__ when class already defines them When @dataclass(init=True) is applied to a class that already defines __init__ in its own __dict__, the generated __init__ is always discarded by _set_new_attribute. However, _init_fn was still called unconditionally, and its field-ordering validation raised TypeError for inherited fields with defaults followed by fields without. Add '__x__' not in cls.__dict__ guards to the if init:, if repr:, and if eq: blocks in _process_class. Methods with 'raise' cells in the design tables (order, frozen) are intentionally NOT skipped early. --- Lib/dataclasses.py | 6 +- .../test_skip_methods_when_defined.py | 185 ++++++++++++++++++ .../Library/2026-07-12-gh-issue-148941.rst | 7 + 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 Lib/test/test_dataclasses/test_skip_methods_when_defined.py create mode 100644 Misc/NEWS.d/next/Library/2026-07-12-gh-issue-148941.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index bd317aa9b0f2f3..9a6a0f363cbad4 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1142,7 +1142,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, func_builder = _FuncBuilder(globals) - if init: + if init and '__init__' not in cls.__dict__: # Does this class have a post-init function? has_post_init = hasattr(cls, _POST_INIT_NAME) @@ -1166,7 +1166,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # used in all of the following methods. field_list = [f for f in fields.values() if f._field_type is _FIELD] - if repr: + if repr and '__repr__' not in cls.__dict__: flds = [f for f in field_list if f.repr] func_builder.add_fn('__repr__', ('self',), @@ -1176,7 +1176,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, locals={'__dataclasses_recursive_repr': recursive_repr}, decorator="@__dataclasses_recursive_repr()") - if eq: + if eq and '__eq__' not in cls.__dict__: # Create __eq__ method. There's no need for a __ne__ method, # since python will call __eq__ and negate it. cmp_fields = (field for field in field_list if field.compare) diff --git a/Lib/test/test_dataclasses/test_skip_methods_when_defined.py b/Lib/test/test_dataclasses/test_skip_methods_when_defined.py new file mode 100644 index 00000000000000..f33052438feb85 --- /dev/null +++ b/Lib/test/test_dataclasses/test_skip_methods_when_defined.py @@ -0,0 +1,185 @@ +"""Tests for gh-148941: dataclass skips method generation when the class +already defines the method in its own __dict__, matching the design tables +documented at the top of dataclasses.py where the intersection of +'parameter=True' and 'class has method in __dict__' has a blank cell +(= no action / skip generation) for __init__, __repr__, and __eq__. + +Methods with 'raise' cells (order, frozen) are intentionally NOT skipped +early — their existing TypeError-on-overwrite behavior is correct and +preserved. +""" + +import unittest +from dataclasses import KW_ONLY, dataclass, field + + +class TestSkipMethodsWhenAlreadyDefined(unittest.TestCase): + """Test that @dataclass skips generating __init__, __repr__, and __eq__ + when the class already defines them in its own __dict__.""" + + # ---- __init__ (the reported bug: gh-148941) ---- + + def test_init_skipped_when_already_defined(self): + # Inherited fields with defaults followed by child fields without + # used to trigger TypeError during _init_fn field-ordering validation, + # even though the class's own __init__ would be preserved by + # _set_new_attribute. + + @dataclass + class Base: + x: int = 0 + + @dataclass + class Child(Base): + y: int + def __init__(self, y): + self.y = y + + obj = Child(5) + self.assertEqual(obj.y, 5) + + def test_init_skipped_with_default_factory(self): + @dataclass + class Base: + items: list = field(default_factory=list) + + @dataclass + class Child(Base): + name: str + def __init__(self, name): + self.name = name + + obj = Child("test") + self.assertEqual(obj.name, "test") + + def test_init_skipped_with_kw_only(self): + @dataclass + class Base: + a: int = 1 + _: KW_ONLY + b: int = 2 + + @dataclass + class Child(Base): + c: int + def __init__(self, c): + self.c = c + + obj = Child(5) + self.assertEqual(obj.c, 5) + + def test_init_skipped_in_three_level_chain(self): + @dataclass + class A: + a: int = 1 + + @dataclass + class B(A): + b: int = 2 + + @dataclass + class C(B): + c: int + def __init__(self, c): + self.c = c + + obj = C(5) + self.assertEqual(obj.c, 5) + + def test_init_not_skipped_when_only_base_defines_it(self): + # __init__ defined in a base class (not in cls.__dict__) should NOT + # prevent generation in the child. + + @dataclass + class Base: + x: int = 0 + def __init__(self, x): + self.x = x + + @dataclass + class Child(Base): + y: int = 1 + + obj = Child(y=2) + self.assertEqual(obj.y, 2) + + def test_init_still_generated_when_not_defined(self): + @dataclass + class Normal: + x: int = 1 + y: int = 2 + + obj = Normal() + self.assertEqual(obj.x, 1) + self.assertEqual(obj.y, 2) + + obj2 = Normal(10, 20) + self.assertEqual(obj2.x, 10) + self.assertEqual(obj2.y, 20) + + # ---- __repr__ ---- + + def test_repr_skipped_when_already_defined(self): + @dataclass + class WithRepr: + x: int + def __repr__(self): + return f"Custom({self.x})" + + obj = WithRepr(42) + self.assertEqual(repr(obj), "Custom(42)") + + def test_repr_still_generated_when_not_defined(self): + @dataclass + class Normal: + x: int = 5 + obj = Normal() + self.assertIn("Normal(x=5)", repr(obj)) + + # ---- __eq__ ---- + + def test_eq_skipped_when_already_defined(self): + @dataclass + class WithEq: + x: int + def __eq__(self, other): + return True # all equal + + a = WithEq(1) + b = WithEq(2) + self.assertEqual(a, b) + + def test_eq_still_generated_when_not_defined(self): + @dataclass + class Normal: + x: int + a = Normal(1) + b = Normal(1) + c = Normal(2) + self.assertEqual(a, b) + self.assertNotEqual(a, c) + + # ---- order and frozen: deliberately NOT skipped (raise is correct) ---- + + def test_order_still_raises_when_defined(self): + # Design table says 'raise' for order=True + has comparison method. + # The early guard does NOT apply here. + with self.assertRaises(TypeError): + @dataclass(order=True) + class BadOrder: + x: int + def __lt__(self, other): + return True + + def test_frozen_still_raises_when_setattr_defined(self): + # Design table says 'raise' for frozen=True + has __setattr__/__delattr__. + with self.assertRaises(TypeError): + @dataclass(frozen=True) + class BadFrozen: + x: int = 1 + def __setattr__(self, name, value): + object.__setattr__(self, name, value) + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-12-gh-issue-148941.rst b/Misc/NEWS.d/next/Library/2026-07-12-gh-issue-148941.rst new file mode 100644 index 00000000000000..5a52efe0c89a6e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-12-gh-issue-148941.rst @@ -0,0 +1,7 @@ +The :func:`~dataclasses.dataclass` decorator now skips generating +:meth:`~object.__init__`, :meth:`~object.__repr__`, and +:meth:`~object.__eq__` when the class already defines them in its own +dictionary, matching the documented design tables. Previously, +``__init__`` generation and its field-ordering validation ran +unconditionally, which could raise :exc:`TypeError` for inherited +fields even when the class's own ``__init__`` would have been preserved.