Skip to content
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

bpo-44674: Use unhashability as a proxy for mutability for default dataclass __init__ arguments. #29867

Merged
merged 8 commits into from
Dec 11, 2021
12 changes: 9 additions & 3 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -712,9 +712,9 @@ Mutable default values
creation they also share this behavior. There is no general way
for Data Classes to detect this condition. Instead, the
:func:`dataclass` decorator will raise a :exc:`TypeError` if it
detects a default parameter of type ``list``, ``dict``, or ``set``.
This is a partial solution, but it does protect against many common
errors.
detects an unhashable default parameter. The assumption is that if
an parameter is unhashable, it is mutable. This is a partial
solution, but it does protect against many common errors.

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

assert D().x is not D().x

.. versionchanged:: 3.11
Instead of looking for and disallowing objects of type ``list``,
``dict``, or ``set``, unhashable objects are now not allowed as
default values. Unhashability is used to approximate
mutability.
5 changes: 3 additions & 2 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,9 @@ def _get_field(cls, a_name, a_type, default_kw_only):
raise TypeError(f'field {f.name} is a ClassVar but specifies '
'kw_only')

# For real fields, disallow mutable defaults for known types.
if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
# For real fields, disallow mutable defaults. Use unhashable as a proxy
# indicator for mutability.
if f._field_type is _FIELD and f.default.__hash__ is None:
raise ValueError(f'mutable default {type(f.default)} for field '
f'{f.name} is not allowed: use default_factory')

Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,29 @@ class C:
self.assertNotEqual(C(3), C(4, 10))
self.assertNotEqual(C(3, 10), C(4, 10))

def test_no_unhashable_default(self):
# See bpo-44674.
class Unhashable:
__hash__ = None

class Hashable:
def __hash__(self):
return 0

with self.assertRaisesRegex(ValueError,
f'mutable default .* for field '
'a is not allowed'):
@dataclass
class A:
a: dict = {}

with self.assertRaisesRegex(ValueError,
f'mutable default .* for field '
'a is not allowed'):
@dataclass
class A:
a: Any = Unhashable()

def test_hash_field_rules(self):
# Test all 6 cases of:
# hash=True/False/None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Change how dataclasses disallows mutable default values. It used to use a
list of known types (list, dict, set). Now it disallows unhashable objects
to be defaults. It's using unhashability as a proxy for mutability.