Skip to content

Commit 5a7a1d8

Browse files
gh-74876: Support pickling weak references and weak containers
* weakref.ref is pickled by the pickler itself, as weakref.ref(referent). The referent is pickled weakly: it must be strongly referenced elsewhere in the same pickle, otherwise pickling raises PicklingError. A reference to a dead object is pickled by name, as the new weakref._dead_ref singleton. The callback is not preserved. * WeakMethod can now be pickled and copied: it reduces to a weak reference to the object the method is bound to. * WeakSet, WeakValueDictionary and WeakKeyDictionary can now be pickled: they reduce their elements, values and keys to weak references. WeakSet can now also be copied. Only existing opcodes are used, so no new pickle protocol is needed. The detection of weak references without a strong reference in the pickle is conservative: valid but exotic reference graphs may be refused. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 93beea7 commit 5a7a1d8

12 files changed

Lines changed: 660 additions & 21 deletions

File tree

Doc/library/pickle.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,11 @@ The following types can be pickled:
517517
* classes accessible from the top level of a module;
518518

519519
* instances of such classes for which the result of calling :meth:`~object.__getstate__`
520-
is picklable (see section :ref:`pickle-inst` for details).
520+
is picklable (see section :ref:`pickle-inst` for details);
521+
522+
* weak references and the weak containers from the :mod:`weakref` module,
523+
provided that the weakly referenced objects are strongly referenced
524+
elsewhere in the same pickle (see :class:`weakref.ref`).
521525

522526
Attempts to pickle unpicklable objects will raise the :exc:`PicklingError`
523527
exception; when this happens, an unspecified number of bytes may have already

Doc/library/weakref.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ See :ref:`__slots__ documentation <slots>` for details.
126126
Weak references are :ref:`generic <generics>` over the type of the object they
127127
reference.
128128

129+
A weak reference can be pickled
130+
if the referent is strongly referenced elsewhere in the same pickle;
131+
otherwise pickling raises :exc:`pickle.PicklingError`.
132+
The unpickled weak reference refers to the unpickled referent.
133+
A reference to a dead object unpickles as a reference to a dead object.
134+
The *callback* is not preserved.
135+
136+
Weak references are atomic for the :mod:`copy` module:
137+
:func:`copy.copy` and :func:`copy.deepcopy` return
138+
the original weak reference without copying the referent.
139+
129140
.. attribute:: __callback__
130141

131142
This read-only attribute returns the callback currently associated to the
@@ -138,6 +149,9 @@ See :ref:`__slots__ documentation <slots>` for details.
138149
.. versionchanged:: next
139150
Raise :exc:`!TypeError` if *callback* is not callable or ``None``.
140151

152+
.. versionchanged:: next
153+
Weak references can now be pickled.
154+
141155

142156
.. function:: proxy(object[, callback])
143157

@@ -203,9 +217,16 @@ See :ref:`__slots__ documentation <slots>` for details.
203217
>>> d[k2] = 2 # d = {k2: 2}
204218
>>> del k1 # d = {k2: 2}
205219

220+
A :class:`!WeakKeyDictionary` can be copied,
221+
and pickled if the keys are strongly referenced
222+
elsewhere in the same pickle (see :class:`ref`).
223+
206224
.. versionchanged:: 3.9
207225
Added support for ``|`` and ``|=`` operators, as specified in :pep:`584`.
208226

227+
.. versionchanged:: next
228+
:class:`!WeakKeyDictionary` can now be pickled.
229+
209230
:class:`WeakKeyDictionary` objects have an additional method that
210231
exposes the internal references directly. The references are not guaranteed to
211232
be "live" at the time they are used, so the result of calling the references
@@ -224,9 +245,16 @@ than needed.
224245
Mapping class that references values weakly. Entries in the dictionary will be
225246
discarded when no strong reference to the value exists any more.
226247

248+
A :class:`!WeakValueDictionary` can be copied,
249+
and pickled if the values are strongly referenced
250+
elsewhere in the same pickle (see :class:`ref`).
251+
227252
.. versionchanged:: 3.9
228253
Added support for ``|`` and ``|=`` operators, as specified in :pep:`584`.
229254

255+
.. versionchanged:: next
256+
:class:`!WeakValueDictionary` can now be pickled.
257+
230258
:class:`WeakValueDictionary` objects have an additional method that has the
231259
same issues as the :meth:`WeakKeyDictionary.keyrefs` method.
232260

@@ -241,6 +269,14 @@ same issues as the :meth:`WeakKeyDictionary.keyrefs` method.
241269
Set class that keeps weak references to its elements. An element will be
242270
discarded when no strong reference to it exists any more.
243271

272+
A :class:`!WeakSet` can be pickled if the elements are strongly
273+
referenced elsewhere in the same pickle (see :class:`ref`).
274+
A copy or a deep copy shares the elements with the original;
275+
user-defined attributes are copied or deep-copied.
276+
277+
.. versionchanged:: next
278+
:class:`!WeakSet` can now be pickled and copied.
279+
244280

245281
.. class:: WeakMethod(method[, callback])
246282

@@ -270,8 +306,16 @@ same issues as the :meth:`WeakKeyDictionary.keyrefs` method.
270306

271307
*callback* is the same as the parameter of the same name to the :func:`ref` function.
272308

309+
A :class:`!WeakMethod` can be copied,
310+
and pickled if the object the method is bound to is strongly referenced
311+
elsewhere in the same pickle (see :class:`ref`).
312+
A copy or a deep copy is an equal weak method to the same object.
313+
273314
.. versionadded:: 3.4
274315

316+
.. versionchanged:: next
317+
:class:`!WeakMethod` can now be pickled and copied.
318+
275319
.. class:: finalize(obj, func, /, *args, **kwargs)
276320

277321
Return a callable finalizer object which will be called when *obj*

Doc/whatsnew/3.16.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,17 @@ os
341341
(Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)
342342

343343

344+
pickle
345+
------
346+
347+
* Weak references (:class:`weakref.ref`), :class:`weakref.WeakMethod`,
348+
:class:`weakref.WeakSet`, :class:`weakref.WeakValueDictionary` and
349+
:class:`weakref.WeakKeyDictionary` can now be pickled if the weakly
350+
referenced objects are strongly referenced elsewhere in the same pickle;
351+
otherwise pickling raises :exc:`pickle.PicklingError`.
352+
:class:`!WeakMethod` and :class:`!WeakSet` can now also be copied.
353+
(Contributed by Serhiy Storchaka in :gh:`74876`.)
354+
344355
re
345356
--
346357

Lib/_weakrefset.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010

1111
class WeakSet:
12+
# Slots give __getstate__() a uniform (dict, slots) shape for __reduce__.
13+
# __dict__ carries user attributes; add() needs __weakref__.
14+
__slots__ = '__dict__', '__weakref__', 'data', '_remove'
15+
1216
def __init__(self, data=None):
1317
self.data = set()
1418

@@ -40,7 +44,14 @@ def __contains__(self, item):
4044
return wr in self.data
4145

4246
def __reduce__(self):
43-
return self.__class__, (list(self),), self.__getstate__()
47+
# Pickle the elements as weak references; data and _remove are
48+
# rebuilt by the reconstructor. Copying shares the elements.
49+
dict, slots = self.__getstate__()
50+
for name in ('data', '_remove'):
51+
slots.pop(name, None)
52+
state = (dict, slots) if dict or slots else None
53+
return (_restore_weakset, (self.__class__, [ref(item) for item in self]),
54+
state)
4455

4556
def add(self, item):
4657
self.data.add(ref(item, self._remove))
@@ -145,3 +156,10 @@ def __repr__(self):
145156
return repr(self.data)
146157

147158
__class_getitem__ = classmethod(GenericAlias)
159+
160+
161+
def _restore_weakset(cls, refs):
162+
self = cls.__new__(cls)
163+
WeakSet.__init__(self)
164+
self.update(item for r in refs for item in [r()] if item is not None)
165+
return self

Lib/pickle.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from struct import pack, unpack
3333
import io
3434
import codecs
35+
import weakref
3536
import _compat_pickle
3637

3738
__all__ = ["PickleError", "PicklingError", "UnpicklingError", "Pickler",
@@ -485,6 +486,9 @@ def __init__(self, file, protocol=None, *, fix_imports=True,
485486
self.write = self.framer.write
486487
self._write_large_bytes = self.framer.write_large_bytes
487488
self.memo = {}
489+
# Ids of objects reached so far only through weak references; any
490+
# left at the end of dump() is an error. See save_weakref.
491+
self._weak_only = set()
488492
self.proto = int(protocol)
489493
self.bin = protocol >= 1
490494
self.fast = 0
@@ -499,6 +503,7 @@ def clear_memo(self):
499503
useful when re-using picklers.
500504
"""
501505
self.memo.clear()
506+
self._weak_only.clear()
502507

503508
def dump(self, obj):
504509
"""Write a pickled representation of obj to the open file."""
@@ -512,6 +517,14 @@ def dump(self, obj):
512517
if self.proto >= 4:
513518
self.framer.start_framing()
514519
self.save(obj)
520+
if self._weak_only:
521+
# These referents would be collected right after loading. Report
522+
# distinct type names: referents may be numerous and unhashable.
523+
names = {_T(self.memo[i][1])
524+
for i in self._weak_only if i in self.memo}
525+
raise PicklingError(
526+
"cannot pickle weak reference(s) whose referent(s) have no "
527+
"strong reference in the pickle: " + ", ".join(sorted(names)))
515528
self.write(STOP)
516529
self.framer.end_framing()
517530

@@ -559,7 +572,7 @@ def get(self, i):
559572

560573
return GET + repr(i).encode("ascii") + b'\n'
561574

562-
def save(self, obj, save_persistent_id=True):
575+
def save(self, obj, save_persistent_id=True, weak=False):
563576
self.framer.commit_frame()
564577

565578
# Check for persistent id (defined by a subclass)
@@ -572,6 +585,9 @@ def save(self, obj, save_persistent_id=True):
572585
# Check the memo
573586
x = self.memo.get(id(obj))
574587
if x is not None:
588+
# A strong reference anchors the object: no longer weak-only.
589+
if not weak:
590+
self._weak_only.discard(id(obj))
575591
self.write(self.get(x[0]))
576592
return
577593

@@ -633,6 +649,12 @@ def save(self, obj, save_persistent_id=True):
633649
exc.add_note(f'when serializing {_T(obj)} object')
634650
raise
635651

652+
# A completed non-weak save anchors obj. Only save_reduce() memoizes
653+
# an object after its arguments, so only here can a weak reference to
654+
# a still-in-progress obj have tagged it.
655+
if not weak:
656+
self._weak_only.discard(id(obj))
657+
636658
def persistent_id(self, obj):
637659
# This exists so a subclass can override it
638660
return None
@@ -1264,6 +1286,40 @@ def save_type(self, obj):
12641286
dispatch[FunctionType] = save_global
12651287
dispatch[type] = save_type
12661288

1289+
def save_weakref(self, obj):
1290+
# Reconstruct as weakref.ref(referent). The referent is saved
1291+
# *weakly* (no anchoring, see save()) and tagged weak-only only after
1292+
# its whole subtree, so a strong cycle inside it cannot anchor it.
1293+
referent = obj()
1294+
if referent is None:
1295+
# All dead references are interchangeable: pickle by name as the
1296+
# weakref._dead_ref singleton.
1297+
self.write(GLOBAL + b'weakref\n_dead_ref\n')
1298+
return
1299+
1300+
save = self.save
1301+
write = self.write
1302+
save(weakref.ref)
1303+
# A referent already in the memo is anchored or already tracked;
1304+
# otherwise it is tagged below, until a strong save of it.
1305+
anchored = id(referent) in self.memo
1306+
# Build the 1-tuple (referent,); TUPLE1 needs protocol 2.
1307+
if self.proto < 2:
1308+
write(MARK)
1309+
save(referent, weak=True)
1310+
if not anchored and id(referent) in self.memo:
1311+
self._weak_only.add(id(referent))
1312+
write(TUPLE1 if self.proto >= 2 else TUPLE)
1313+
write(REDUCE)
1314+
# The referent's subtree may have pickled this very (cached) weakref
1315+
# already; reuse the memoized one.
1316+
if id(obj) in self.memo:
1317+
write(POP + self.get(self.memo[id(obj)][0]))
1318+
else:
1319+
self.memoize(obj)
1320+
1321+
dispatch[weakref.ReferenceType] = save_weakref
1322+
12671323

12681324
# Unpickling machinery
12691325

Lib/test/pickletester.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,9 +1755,61 @@ def t():
17551755
[ToBeUnpickled] * 2)
17561756

17571757

1758+
class WeakrefTarget:
1759+
# Weakly referenceable, picklable, and not hashable -- the weak-only
1760+
# error path must not assume hashable referents.
1761+
__hash__ = None
1762+
1763+
17581764
class AbstractPicklingErrorTests:
17591765
# Subclass must define self.dumps, self.pickler.
17601766

1767+
def test_unpicklable_weakref_referent(self):
1768+
# Pickling a weak reference pickles its referent too, so it fails when
1769+
# the referent is not picklable (a lambda).
1770+
f = lambda: 42
1771+
wr = weakref.ref(f)
1772+
for proto in protocols:
1773+
with self.subTest(proto=proto):
1774+
self.assertRaises(pickle.PicklingError, self.dumps, wr, proto)
1775+
1776+
def test_weakref_without_strong_ref(self):
1777+
# The error names the distinct referent types: bounded however many
1778+
# referents there are, and not requiring them to be hashable.
1779+
referents = [WeakrefTarget() for _ in range(5)]
1780+
wrs = [weakref.ref(obj) for obj in referents]
1781+
typename = f'{WeakrefTarget.__module__}.{WeakrefTarget.__qualname__}'
1782+
for proto in protocols:
1783+
with self.subTest(proto=proto):
1784+
with self.assertRaises(pickle.PicklingError) as cm:
1785+
self.dumps(wrs, proto)
1786+
self.assertEqual(str(cm.exception),
1787+
'cannot pickle weak reference(s) whose referent(s) have no '
1788+
'strong reference in the pickle: ' + typename)
1789+
1790+
def test_weakref_reused_pickler(self):
1791+
# Both re-initializing the pickler and clearing its memo reset the
1792+
# weak-only bookkeeping left by a refused dump.
1793+
obj = WeakrefTarget()
1794+
wr = weakref.ref(obj)
1795+
p = self.pickler(io.BytesIO())
1796+
self.assertRaises(pickle.PicklingError, p.dump, wr)
1797+
f = io.BytesIO()
1798+
p.__init__(f)
1799+
p.dump([obj, wr])
1800+
obj2, wr2 = pickle.loads(f.getvalue())
1801+
self.assertIs(wr2(), obj2)
1802+
1803+
f = io.BytesIO()
1804+
p = self.pickler(f)
1805+
self.assertRaises(pickle.PicklingError, p.dump, wr)
1806+
p.clear_memo()
1807+
f.seek(0)
1808+
f.truncate()
1809+
p.dump([obj, wr])
1810+
obj2, wr2 = pickle.loads(f.getvalue())
1811+
self.assertIs(wr2(), obj2)
1812+
17611813
def test_bad_reduce_result(self):
17621814
obj = REX([print, ()])
17631815
for proto in protocols:
@@ -3529,6 +3581,47 @@ def test_newobj_proxies(self):
35293581
self.assertEqual(B(x), B(y), detail)
35303582
self.assertEqual(x.__dict__, y.__dict__, detail)
35313583

3584+
def test_weakref(self):
3585+
obj = WeakrefTarget()
3586+
wr = weakref.ref(obj)
3587+
for proto in protocols:
3588+
with self.subTest(proto=proto):
3589+
obj2, wr2 = self.loads(self.dumps([obj, wr], proto))
3590+
self.assertIs(type(wr2), weakref.ref)
3591+
self.assertIs(wr2(), obj2)
3592+
# Identity holds regardless of pickling order.
3593+
wr2, obj2 = self.loads(self.dumps([wr, obj], proto))
3594+
self.assertIs(wr2(), obj2)
3595+
3596+
def test_weakref_callback(self):
3597+
# The callback is not preserved: pickling it strongly would defeat
3598+
# the weak-only detection.
3599+
obj = WeakrefTarget()
3600+
wr = weakref.ref(obj, lambda r: None)
3601+
for proto in protocols:
3602+
with self.subTest(proto=proto):
3603+
obj2, wr2 = self.loads(self.dumps([obj, wr], proto))
3604+
self.assertIs(wr2(), obj2)
3605+
self.assertIsNone(wr2.__callback__)
3606+
3607+
def test_dead_weakref(self):
3608+
# A dead reference stays dead, and all dead references unpickle as
3609+
# the same object.
3610+
obj = WeakrefTarget()
3611+
wr = weakref.ref(obj)
3612+
obj2 = WeakrefTarget()
3613+
wr2 = weakref.ref(obj2)
3614+
del obj, obj2
3615+
support.gc_collect()
3616+
self.assertIsNone(wr())
3617+
for proto in protocols:
3618+
with self.subTest(proto=proto):
3619+
a, b = self.loads(self.dumps([wr, wr2], proto))
3620+
self.assertIs(type(a), weakref.ref)
3621+
self.assertIs(a, b)
3622+
support.gc_collect()
3623+
self.assertIsNone(a())
3624+
35323625
def test_newobj_overridden_new(self):
35333626
# Test that Python class with C implemented __new__ is pickleable
35343627
for proto in protocols:

Lib/test/test_pickle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ class SizeofTests(unittest.TestCase):
507507
check_sizeof = support.check_sizeof
508508

509509
def test_pickler(self):
510-
basesize = support.calcobjsize('7P2n3i2n4i2P')
510+
basesize = support.calcobjsize('7P2n3i2n4i2P1n')
511511
p = _pickle.Pickler(io.BytesIO())
512512
self.assertEqual(object.__sizeof__(p), basesize)
513513
MT_size = struct.calcsize('3nP0n')

0 commit comments

Comments
 (0)