Skip to content

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

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

Merged
merged 8 commits into from
Dec 11, 2021
Merged
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
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
a value 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.
6 changes: 4 additions & 2 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,10 @@ 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. Read the __hash__ attribute from the class,
# not the instance.
if f._field_type is _FIELD and f.default.__class__.__hash__ is None:
raise ValueError(f'mutable default {type(f.default)} for field '
f'{f.name} is not allowed: use default_factory')

Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,32 @@ 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

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

with self.assertRaisesRegex(ValueError, unhashable_re):
@dataclass
class A:
a: Any = Unhashable()

# Make sure that the machinery looking for hashability is using the
# class's __hash__, not the instance's __hash__.
with self.assertRaisesRegex(ValueError, unhashable_re):
unhashable = Unhashable()
# This shouldn't make the variable hashable.
unhashable.__hash__ = lambda: 0
@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,6 @@
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. Patch by Eric V. Smith, idea by Raymond
Hettinger.