Skip to content

Commit 2462afa

Browse files
committed
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.
1 parent 0023d5b commit 2462afa

3 files changed

Lines changed: 195 additions & 3 deletions

File tree

Lib/dataclasses.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
11421142

11431143
func_builder = _FuncBuilder(globals)
11441144

1145-
if init:
1145+
if init and '__init__' not in cls.__dict__:
11461146
# Does this class have a post-init function?
11471147
has_post_init = hasattr(cls, _POST_INIT_NAME)
11481148

@@ -1166,7 +1166,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
11661166
# used in all of the following methods.
11671167
field_list = [f for f in fields.values() if f._field_type is _FIELD]
11681168

1169-
if repr:
1169+
if repr and '__repr__' not in cls.__dict__:
11701170
flds = [f for f in field_list if f.repr]
11711171
func_builder.add_fn('__repr__',
11721172
('self',),
@@ -1176,7 +1176,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
11761176
locals={'__dataclasses_recursive_repr': recursive_repr},
11771177
decorator="@__dataclasses_recursive_repr()")
11781178

1179-
if eq:
1179+
if eq and '__eq__' not in cls.__dict__:
11801180
# Create __eq__ method. There's no need for a __ne__ method,
11811181
# since python will call __eq__ and negate it.
11821182
cmp_fields = (field for field in field_list if field.compare)
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Tests for gh-148941: dataclass skips method generation when the class
2+
already defines the method in its own __dict__, matching the design tables
3+
documented at the top of dataclasses.py where the intersection of
4+
'parameter=True' and 'class has method in __dict__' has a blank cell
5+
(= no action / skip generation) for __init__, __repr__, and __eq__.
6+
7+
Methods with 'raise' cells (order, frozen) are intentionally NOT skipped
8+
early — their existing TypeError-on-overwrite behavior is correct and
9+
preserved.
10+
"""
11+
12+
import unittest
13+
from dataclasses import KW_ONLY, dataclass, field
14+
15+
16+
class TestSkipMethodsWhenAlreadyDefined(unittest.TestCase):
17+
"""Test that @dataclass skips generating __init__, __repr__, and __eq__
18+
when the class already defines them in its own __dict__."""
19+
20+
# ---- __init__ (the reported bug: gh-148941) ----
21+
22+
def test_init_skipped_when_already_defined(self):
23+
# Inherited fields with defaults followed by child fields without
24+
# used to trigger TypeError during _init_fn field-ordering validation,
25+
# even though the class's own __init__ would be preserved by
26+
# _set_new_attribute.
27+
28+
@dataclass
29+
class Base:
30+
x: int = 0
31+
32+
@dataclass
33+
class Child(Base):
34+
y: int
35+
def __init__(self, y):
36+
self.y = y
37+
38+
obj = Child(5)
39+
self.assertEqual(obj.y, 5)
40+
41+
def test_init_skipped_with_default_factory(self):
42+
@dataclass
43+
class Base:
44+
items: list = field(default_factory=list)
45+
46+
@dataclass
47+
class Child(Base):
48+
name: str
49+
def __init__(self, name):
50+
self.name = name
51+
52+
obj = Child("test")
53+
self.assertEqual(obj.name, "test")
54+
55+
def test_init_skipped_with_kw_only(self):
56+
@dataclass
57+
class Base:
58+
a: int = 1
59+
_: KW_ONLY
60+
b: int = 2
61+
62+
@dataclass
63+
class Child(Base):
64+
c: int
65+
def __init__(self, c):
66+
self.c = c
67+
68+
obj = Child(5)
69+
self.assertEqual(obj.c, 5)
70+
71+
def test_init_skipped_in_three_level_chain(self):
72+
@dataclass
73+
class A:
74+
a: int = 1
75+
76+
@dataclass
77+
class B(A):
78+
b: int = 2
79+
80+
@dataclass
81+
class C(B):
82+
c: int
83+
def __init__(self, c):
84+
self.c = c
85+
86+
obj = C(5)
87+
self.assertEqual(obj.c, 5)
88+
89+
def test_init_not_skipped_when_only_base_defines_it(self):
90+
# __init__ defined in a base class (not in cls.__dict__) should NOT
91+
# prevent generation in the child.
92+
93+
@dataclass
94+
class Base:
95+
x: int = 0
96+
def __init__(self, x):
97+
self.x = x
98+
99+
@dataclass
100+
class Child(Base):
101+
y: int = 1
102+
103+
obj = Child(y=2)
104+
self.assertEqual(obj.y, 2)
105+
106+
def test_init_still_generated_when_not_defined(self):
107+
@dataclass
108+
class Normal:
109+
x: int = 1
110+
y: int = 2
111+
112+
obj = Normal()
113+
self.assertEqual(obj.x, 1)
114+
self.assertEqual(obj.y, 2)
115+
116+
obj2 = Normal(10, 20)
117+
self.assertEqual(obj2.x, 10)
118+
self.assertEqual(obj2.y, 20)
119+
120+
# ---- __repr__ ----
121+
122+
def test_repr_skipped_when_already_defined(self):
123+
@dataclass
124+
class WithRepr:
125+
x: int
126+
def __repr__(self):
127+
return f"Custom({self.x})"
128+
129+
obj = WithRepr(42)
130+
self.assertEqual(repr(obj), "Custom(42)")
131+
132+
def test_repr_still_generated_when_not_defined(self):
133+
@dataclass
134+
class Normal:
135+
x: int = 5
136+
obj = Normal()
137+
self.assertIn("Normal(x=5)", repr(obj))
138+
139+
# ---- __eq__ ----
140+
141+
def test_eq_skipped_when_already_defined(self):
142+
@dataclass
143+
class WithEq:
144+
x: int
145+
def __eq__(self, other):
146+
return True # all equal
147+
148+
a = WithEq(1)
149+
b = WithEq(2)
150+
self.assertEqual(a, b)
151+
152+
def test_eq_still_generated_when_not_defined(self):
153+
@dataclass
154+
class Normal:
155+
x: int
156+
a = Normal(1)
157+
b = Normal(1)
158+
c = Normal(2)
159+
self.assertEqual(a, b)
160+
self.assertNotEqual(a, c)
161+
162+
# ---- order and frozen: deliberately NOT skipped (raise is correct) ----
163+
164+
def test_order_still_raises_when_defined(self):
165+
# Design table says 'raise' for order=True + has comparison method.
166+
# The early guard does NOT apply here.
167+
with self.assertRaises(TypeError):
168+
@dataclass(order=True)
169+
class BadOrder:
170+
x: int
171+
def __lt__(self, other):
172+
return True
173+
174+
def test_frozen_still_raises_when_setattr_defined(self):
175+
# Design table says 'raise' for frozen=True + has __setattr__/__delattr__.
176+
with self.assertRaises(TypeError):
177+
@dataclass(frozen=True)
178+
class BadFrozen:
179+
x: int = 1
180+
def __setattr__(self, name, value):
181+
object.__setattr__(self, name, value)
182+
183+
184+
if __name__ == '__main__':
185+
unittest.main()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
The :func:`~dataclasses.dataclass` decorator now skips generating
2+
:meth:`~object.__init__`, :meth:`~object.__repr__`, and
3+
:meth:`~object.__eq__` when the class already defines them in its own
4+
dictionary, matching the documented design tables. Previously,
5+
``__init__`` generation and its field-ordering validation ran
6+
unconditionally, which could raise :exc:`TypeError` for inherited
7+
fields even when the class's own ``__init__`` would have been preserved.

0 commit comments

Comments
 (0)