Skip to content

gh-89529: disallow default_factory for fields in dataclasses without __init__ #123070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,10 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
# Otherwise it's a field of some type.
cls_fields.append(_get_field(cls, name, type, kw_only))

# Test whether '__init__' is to be auto-generated or if
# it is provided explicitly by the user.
has_init_method = init or '__init__' in cls.__dict__

for f in cls_fields:
fields[f.name] = f

Expand All @@ -1018,6 +1022,15 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
# sees a real default value, not a Field.
if isinstance(getattr(cls, f.name, None), Field):
if f.default is MISSING:
# https://github.com/python/cpython/issues/89529
if f.default_factory is not MISSING and not has_init_method:
raise ValueError(
f'specifying default_factory for {f.name!r}'
f' requires the @dataclass decorator to be'
f' called with init=True or to implement'
f' an __init__ method'
)

# If there's no default, delete the class attribute.
# This happens if we specify field(repr=False), for
# example (that is, we specified a field object, but
Expand Down
67 changes: 67 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pickle
import inspect
import builtins
import re
import types
import weakref
import traceback
Expand All @@ -18,6 +19,7 @@
from typing import get_type_hints
from collections import deque, OrderedDict, namedtuple, defaultdict
from copy import deepcopy
from itertools import product
from functools import total_ordering, wraps

import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation.
Expand Down Expand Up @@ -1413,6 +1415,71 @@ class C:
C().x
self.assertEqual(factory.call_count, 2)

def test_default_factory_and_init_method_interaction(self):
# See https://github.com/python/cpython/issues/89529.

@dataclass
class BaseWithInit:
x: list

@dataclass(slots=True)
class BaseWithSlots:
x: list

@dataclass(init=False)
class BaseWithOutInit:
x: list

@dataclass(init=False, slots=True)
class BaseWithOutInitWithSlots:
x: list

err = re.escape(
"specifying default_factory for 'x' requires the "
"@dataclass decorator to be called with init=True "
"or to implement an __init__ method"
)

for base_class, slots, field_init in product(
(object, BaseWithInit, BaseWithSlots,
BaseWithOutInit, BaseWithOutInitWithSlots),
(True, False),
(True, False),
):
with self.subTest('generated __init__', base_class=base_class,
init=True, slots=slots, field_init=field_init):
@dataclass(init=True, slots=slots)
class C(base_class):
x: list = field(init=field_init, default_factory=list)
self.assertListEqual(C().x, [])

with self.subTest('user-defined __init__', base_class=base_class,
init=True, slots=slots, field_init=field_init):
@dataclass(init=True, slots=slots)
class C(base_class):
x: list = field(init=field_init, default_factory=list)
def __init__(self, *a, **kw):
# deliberately use something else
self.x = 'hello'
self.assertEqual(C().x, 'hello')

with self.subTest('no generated __init__', base_class=base_class,
init=False, slots=slots, field_init=field_init):
with self.assertRaisesRegex(ValueError, err):
@dataclass(init=False, slots=slots)
class C(base_class):
x: list = field(init=field_init, default_factory=list)

with self.subTest('user-defined __init__', base_class=base_class,
init=False, slots=slots, field_init=field_init):
@dataclass(init=False, slots=slots)
class C(base_class):
x: list = field(init=field_init, default_factory=list)
def __init__(self, *a, **kw):
# deliberately use something else
self.x = 'world'
self.assertEqual(C().x, 'world')

def test_default_factory_not_called_if_value_given(self):
# We need a factory that we can test if it's been called.
factory = Mock()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Disallow ``default_factory`` for dataclass fields if the dataclass does not
have an ``__init__`` method. Patch by Bénédikt Tran.
Loading