From ea1dd417842f0df278787cdbc686a0949a597331 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 22 Oct 2026 05:25:42 +0000 Subject: [PATCH] gh-161106: Use repeat counts in plistlib binary writer struct formats 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. --- Lib/plistlib.py | 8 ++++---- Lib/test/test_plistlib.py | 20 +++++++++++++++++++ ...-10-22-05-20-44.gh-issue-161106.t0ux5q.rst | 3 +++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-05-20-44.gh-issue-161106.t0ux5q.rst diff --git a/Lib/plistlib.py b/Lib/plistlib.py index 822fbcf17b16d08..119fa16ec64f0d2 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 fd6036c033ffb85..22639d46474ef9a 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 000000000000000..78ab254e8fd353a --- /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.