[Python-checkins] bpo-44674: Use unhashability as a proxy for mutability for default dataclass __init__ arguments. (GH-29867)

ericvsmith webhook-mailer at python.org
Sat Dec 11 16:12:26 EST 2021


https://github.com/python/cpython/commit/e029c53e1a408b89a4e3edf30a9b38b094f9c880
commit: e029c53e1a408b89a4e3edf30a9b38b094f9c880
branch: main
author: Eric V. Smith <ericvsmith at users.noreply.github.com>
committer: ericvsmith <ericvsmith at users.noreply.github.com>
date: 2021-12-11T16:12:17-05:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2021-11-29-19-37-20.bpo-44674.NijWLt.rst
M Doc/library/dataclasses.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py

diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst
index b06547074378f..3592429d06a4c 100644
--- a/Doc/library/dataclasses.rst
+++ b/Doc/library/dataclasses.rst
@@ -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::
@@ -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.
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 3f85d859b1642..b327462080f99 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -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')
 
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index ef5009ab11677..69e7685083009 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -501,6 +501,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
diff --git a/Misc/NEWS.d/next/Library/2021-11-29-19-37-20.bpo-44674.NijWLt.rst b/Misc/NEWS.d/next/Library/2021-11-29-19-37-20.bpo-44674.NijWLt.rst
new file mode 100644
index 0000000000000..79e7a08b3b174
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-11-29-19-37-20.bpo-44674.NijWLt.rst
@@ -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.
+



More information about the Python-checkins mailing list