]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145056: Add support for frozendict in dataclass asdict and astuple (#145125)
authorPieter Eendebak <pieter.eendebak@gmail.com>
Thu, 23 Apr 2026 02:39:08 +0000 (04:39 +0200)
committerGitHub <noreply@github.com>
Thu, 23 Apr 2026 02:39:08 +0000 (19:39 -0700)
Doc/library/dataclasses.rst
Lib/dataclasses.py
Lib/test/test_dataclasses/__init__.py
Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst [new file with mode: 0644]

index fd8e0c0bea1cb10b4828db049dfa878691e82f63..0bce3e5b762b8be56437ee96271c4c9a430c92b0 100644 (file)
@@ -371,8 +371,8 @@ Module contents
    Converts the dataclass *obj* to a dict (by using the
    factory function *dict_factory*).  Each dataclass is converted
    to a dict of its fields, as ``name: value`` pairs.  dataclasses, dicts,
-   lists, and tuples are recursed into.  Other objects are copied with
-   :func:`copy.deepcopy`.
+   frozendicts, lists, and tuples are recursed into.  Other objects are copied
+   with :func:`copy.deepcopy`.
 
    Example of using :func:`!asdict` on nested dataclasses::
 
@@ -402,8 +402,8 @@ Module contents
 
    Converts the dataclass *obj* to a tuple (by using the
    factory function *tuple_factory*).  Each dataclass is converted
-   to a tuple of its field values.  dataclasses, dicts, lists, and
-   tuples are recursed into. Other objects are copied with
+   to a tuple of its field values.  dataclasses, dicts, frozendicts, lists,
+   and tuples are recursed into. Other objects are copied with
    :func:`copy.deepcopy`.
 
    Continuing from the previous example::
index 0c7e01cb16b192cb966efe7f9aed95cf5481932c..9d5bed6b96fc49dfc2e24cc1b703afe53bc1ba29 100644 (file)
@@ -1496,7 +1496,8 @@ def asdict(obj, *, dict_factory=dict):
     If given, 'dict_factory' will be used instead of built-in dict.
     The function applies recursively to field values that are
     dataclass instances. This will also look into built-in containers:
-    tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
+    tuples, lists, dicts, and frozendicts. Other objects are copied
+    with 'copy.deepcopy()'.
     """
     if not _is_dataclass_instance(obj):
         raise TypeError("asdict() should be called on dataclass instances")
@@ -1552,7 +1553,7 @@ def _asdict_inner(obj, dict_factory):
             return obj_type(*[_asdict_inner(v, dict_factory) for v in obj])
         else:
             return obj_type(_asdict_inner(v, dict_factory) for v in obj)
-    elif issubclass(obj_type, dict):
+    elif issubclass(obj_type, (dict, frozendict)):
         if hasattr(obj_type, 'default_factory'):
             # obj is a defaultdict, which has a different constructor from
             # dict as it requires the default_factory as its first arg.
@@ -1587,7 +1588,8 @@ def astuple(obj, *, tuple_factory=tuple):
     If given, 'tuple_factory' will be used instead of built-in tuple.
     The function applies recursively to field values that are
     dataclass instances. This will also look into built-in containers:
-    tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
+    tuples, lists, dicts, and frozendicts. Other objects are copied
+    with 'copy.deepcopy()'.
     """
 
     if not _is_dataclass_instance(obj):
@@ -1616,7 +1618,7 @@ def _astuple_inner(obj, tuple_factory):
         # generator (which is not true for namedtuples, handled
         # above).
         return type(obj)(_astuple_inner(v, tuple_factory) for v in obj)
-    elif isinstance(obj, dict):
+    elif isinstance(obj, (dict, frozendict)):
         obj_type = type(obj)
         if hasattr(obj_type, 'default_factory'):
             # obj is a defaultdict, which has a different constructor from
index b44b1da0336d093717df927e178be263add14176..e0cfe3df3e63578d2b62d39a6c8d758abeae8d50 100644 (file)
@@ -1693,17 +1693,24 @@ class TestCase(unittest.TestCase):
         class GroupDict:
             id: int
             users: Dict[str, User]
+        @dataclass
+        class GroupFrozenDict:
+            id: int
+            users: frozendict[str, User]
         a = User('Alice', 1)
         b = User('Bob', 2)
         gl = GroupList(0, [a, b])
         gt = GroupTuple(0, (a, b))
         gd = GroupDict(0, {'first': a, 'second': b})
+        gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b}))
         self.assertEqual(asdict(gl), {'id': 0, 'users': [{'name': 'Alice', 'id': 1},
                                                          {'name': 'Bob', 'id': 2}]})
         self.assertEqual(asdict(gt), {'id': 0, 'users': ({'name': 'Alice', 'id': 1},
                                                          {'name': 'Bob', 'id': 2})})
-        self.assertEqual(asdict(gd), {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1},
-                                                         'second': {'name': 'Bob', 'id': 2}}})
+        expected_dict = {'id': 0, 'users': {'first': {'name': 'Alice', 'id': 1},
+                                            'second': {'name': 'Bob', 'id': 2}}}
+        self.assertEqual(asdict(gd), expected_dict)
+        self.assertEqual(asdict(gfd), expected_dict)
 
     def test_helper_asdict_builtin_object_containers(self):
         @dataclass
@@ -1884,14 +1891,21 @@ class TestCase(unittest.TestCase):
         class GroupDict:
             id: int
             users: Dict[str, User]
+        @dataclass
+        class GroupFrozenDict:
+            id: int
+            users: frozendict[str, User]
         a = User('Alice', 1)
         b = User('Bob', 2)
         gl = GroupList(0, [a, b])
         gt = GroupTuple(0, (a, b))
         gd = GroupDict(0, {'first': a, 'second': b})
+        gfd = GroupFrozenDict(0, frozendict({'first': a, 'second': b}))
         self.assertEqual(astuple(gl), (0, [('Alice', 1), ('Bob', 2)]))
         self.assertEqual(astuple(gt), (0, (('Alice', 1), ('Bob', 2))))
-        self.assertEqual(astuple(gd), (0, {'first': ('Alice', 1), 'second': ('Bob', 2)}))
+        d = {'first': ('Alice', 1), 'second': ('Bob', 2)}
+        self.assertEqual(astuple(gd), (0, d))
+        self.assertEqual(astuple(gfd), (0, frozendict(d)))
 
     def test_helper_astuple_builtin_object_containers(self):
         @dataclass
diff --git a/Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst b/Misc/NEWS.d/next/Library/2026-02-22-19-36-00.gh-issue-145056.TH8nX4.rst
new file mode 100644 (file)
index 0000000..45be010
--- /dev/null
@@ -0,0 +1 @@
+Add support for :class:`frozendict` in :meth:`dataclasses.asdict` and :meth:`dataclasses.astuple`.