From f63f895369d2ccec26569d7b822b9742ea052ad4 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:41:32 -0800 Subject: [PATCH] [3.14] 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 (cherry picked from commit 4b21d154b4864531641f90e1b87108575e673d75) --- 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 41c32cc696f95c..32fd989f11d66e 100644 --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -706,7 +706,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 @@ -853,7 +853,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): keyRefs, valRefs = [], [] @@ -869,8 +869,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 6f048b903667a7..7e556059fa11b8 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 @@ -1018,6 +1019,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.