https://github.com/python/cpython/commit/4b21d154b4864531641f90e1b87108575e673d75
commit: 4b21d154b4864531641f90e1b87108575e673d75
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-11-01T20:41:32Z
summary:

gh-161106: Use repeat counts in plistlib binary writer struct formats 
(GH-161107)

Compiled struct formats with one code per item stay in the struct
module's cache and take about 33 bytes per item.  Use the repeat-count
form the reader already uses; the output is unchanged.

ANT-2026-1L9EBYNF

files:
A Misc/NEWS.d/next/Library/2026-10-22-05-20-44.gh-issue-161106.t0ux5q.rst
M Lib/plistlib.py
M Lib/test/test_plistlib.py

diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index 822fbcf17b16d0..119fa16ec64f0d 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -699,7 +699,7 @@ def write(self, value):
         top_object = self._getrefnum(value)
         offset_table_offset = self._fp.tell()
         offset_size = _count_to_size(offset_table_offset)
-        offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
+        offset_format = f'>{num_objects}{_BINARY_FORMAT[offset_size]}'
         self._fp.write(struct.pack(offset_format, *self._object_offsets))
 
         # Write trailer
@@ -846,7 +846,7 @@ def _write_object(self, value):
             refs = [self._getrefnum(o) for o in value]
             s = len(refs)
             self._write_size(0xA0, s)
-            self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
+            self._fp.write(struct.pack(f'>{s}{self._ref_format}', *refs))
 
         elif isinstance(value, (dict, frozendict)):
             keyRefs, valRefs = [], []
@@ -862,8 +862,8 @@ def _write_object(self, value):
 
             s = len(keyRefs)
             self._write_size(0xD0, s)
-            self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
-            self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
+            self._fp.write(struct.pack(f'>{s}{self._ref_format}', *keyRefs))
+            self._fp.write(struct.pack(f'>{s}{self._ref_format}', *valRefs))
 
         else:
             raise TypeError(value)
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
index fd6036c033ffb8..22639d46474ef9 100644
--- a/Lib/test/test_plistlib.py
+++ b/Lib/test/test_plistlib.py
@@ -3,6 +3,7 @@
 import operator
 import pickle
 import struct
+import tracemalloc
 import unittest
 import plistlib
 import os
@@ -1037,6 +1038,25 @@ def test_dump_duplicates(self):
                 data = plistlib.dumps([x]*1000, fmt=plistlib.FMT_BINARY)
                 self.assertLess(len(data), 1100, repr(data))
 
+    def test_dump_large_container_memory(self):
+        # Writing a large container should not leave memory proportional
+        # to its size allocated after the call returns.
+        if not tracemalloc.is_tracing():
+            tracemalloc.start()
+            self.addCleanup(tracemalloc.stop)
+        n = 20_000
+        for make in (lambda n: [True] * n,
+                     lambda n: dict.fromkeys(map(str, range(n)), True)):
+            x = make(n)
+            with self.subTest(type=type(x).__name__):
+                # Warm up with a slightly smaller container, so that
+                # memory kept by free lists is not counted.
+                plistlib.dumps(make(n - 1), fmt=plistlib.FMT_BINARY)
+                before, _ = tracemalloc.get_traced_memory()
+                plistlib.dumps(x, fmt=plistlib.FMT_BINARY)
+                after, _ = tracemalloc.get_traced_memory()
+                self.assertLess(after - before, n)
+
     def test_identity(self):
         for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
                   datetime.datetime(2004, 10, 26, 10, 33, 33),
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-22-05-20-44.gh-issue-161106.t0ux5q.rst 
b/Misc/NEWS.d/next/Library/2026-10-22-05-20-44.gh-issue-161106.t0ux5q.rst
new file mode 100644
index 00000000000000..78ab254e8fd353
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-22-05-20-44.gh-issue-161106.t0ux5q.rst
@@ -0,0 +1,3 @@
+Fix :func:`plistlib.dump` and :func:`plistlib.dumps` with
+:data:`~plistlib.FMT_BINARY` holding on to memory proportional to the size of
+large arrays and dictionaries after the call returns.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to