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]