Skip to content

Commit e029c53

Browse files
authored
bpo-44674: Use unhashability as a proxy for mutability for default dataclass __init__ arguments. (GH-29867)
`@dataclass` in 3.10 prohibits using list, dict, or set as default values. It does this to avoid the mutable default problem. This test is both too strict, and not strict enough. Too strict, because some immutable subclasses should be safe, and not strict enough, because other mutable types should be prohibited. With this change applied, `@dataclass` now uses unhashability as a proxy for mutability: if objects aren't hashable, they're assumed to be mutable.
1 parent bfc59ed commit e029c53

File tree

4 files changed

+45
-5
lines changed

4 files changed

+45
-5
lines changed

Doc/library/dataclasses.rst

+9-3
Original file line numberDiff line numberDiff line change
@@ -712,9 +712,9 @@ Mutable default values
712712
creation they also share this behavior. There is no general way
713713
for Data Classes to detect this condition. Instead, the
714714
:func:`dataclass` decorator will raise a :exc:`TypeError` if it
715-
detects a default parameter of type ``list``, ``dict``, or ``set``.
716-
This is a partial solution, but it does protect against many common
717-
errors.
715+
detects an unhashable default parameter. The assumption is that if
716+
a value is unhashable, it is mutable. This is a partial solution,
717+
but it does protect against many common errors.
718718

719719
Using default factory functions is a way to create new instances of
720720
mutable types as default values for fields::
@@ -724,3 +724,9 @@ Mutable default values
724724
x: list = field(default_factory=list)
725725

726726
assert D().x is not D().x
727+
728+
.. versionchanged:: 3.11
729+
Instead of looking for and disallowing objects of type ``list``,
730+
``dict``, or ``set``, unhashable objects are now not allowed as
731+
default values. Unhashability is used to approximate
732+
mutability.

Lib/dataclasses.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -808,8 +808,10 @@ def _get_field(cls, a_name, a_type, default_kw_only):
808808
raise TypeError(f'field {f.name} is a ClassVar but specifies '
809809
'kw_only')
810810

811-
# For real fields, disallow mutable defaults for known types.
812-
if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
811+
# For real fields, disallow mutable defaults. Use unhashable as a proxy
812+
# indicator for mutability. Read the __hash__ attribute from the class,
813+
# not the instance.
814+
if f._field_type is _FIELD and f.default.__class__.__hash__ is None:
813815
raise ValueError(f'mutable default {type(f.default)} for field '
814816
f'{f.name} is not allowed: use default_factory')
815817

Lib/test/test_dataclasses.py

+26
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,32 @@ class C:
501501
self.assertNotEqual(C(3), C(4, 10))
502502
self.assertNotEqual(C(3, 10), C(4, 10))
503503

504+
def test_no_unhashable_default(self):
505+
# See bpo-44674.
506+
class Unhashable:
507+
__hash__ = None
508+
509+
unhashable_re = 'mutable default .* for field a is not allowed'
510+
with self.assertRaisesRegex(ValueError, unhashable_re):
511+
@dataclass
512+
class A:
513+
a: dict = {}
514+
515+
with self.assertRaisesRegex(ValueError, unhashable_re):
516+
@dataclass
517+
class A:
518+
a: Any = Unhashable()
519+
520+
# Make sure that the machinery looking for hashability is using the
521+
# class's __hash__, not the instance's __hash__.
522+
with self.assertRaisesRegex(ValueError, unhashable_re):
523+
unhashable = Unhashable()
524+
# This shouldn't make the variable hashable.
525+
unhashable.__hash__ = lambda: 0
526+
@dataclass
527+
class A:
528+
a: Any = unhashable
529+
504530
def test_hash_field_rules(self):
505531
# Test all 6 cases of:
506532
# hash=True/False/None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Change how dataclasses disallows mutable default values. It used to
2+
use a list of known types (list, dict, set). Now it disallows
3+
unhashable objects to be defaults. It's using unhashability as a
4+
proxy for mutability. Patch by Eric V. Smith, idea by Raymond
5+
Hettinger.
6+

0 commit comments

Comments
 (0)