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.