uqfoundation/dill

threading.Thread fails to pickle in python > 3.13.0a5

Opened this issue · 4 comments

Starting in python 3.13.0a5, a threading.Thread instance is not serializable. This is due to the thread attribute pointing to an unserializable _thread._ThreadHandle instance, where in earlier versions the _thread instance pointed to None. Deleting the _ThreadHandle enables the Thread to be serializable again. Presumably, there's a good way to obtain an appropriate thread handle from the thread or using the threading module.

Python 3.13.0a5 (main, Mar 16 2024, 18:36:37) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import threading
>>> t = threading.Thread()
>>> t.start()
>>> import dill
>>> dill.dumps(t)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    dill.dumps(t)
    ~~~~~~~~~~^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 280, in dumps
    dump(obj, file, protocol, byref, fmode, recurse, **kwds)#, strictio)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 252, in dump
    Pickler(file, protocol, **_kwds).dump(obj)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 420, in dump
    StockPickler.dump(self, obj)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 483, in dump
    self.save(obj)
    ~~~~~~~~~^^^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 414, in save
    StockPickler.save(self, obj, save_persistent_id)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 599, in save
    self.save_reduce(obj=obj, *rv)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 713, in save_reduce
    save(state)
    ~~~~^^^^^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 414, in save
    StockPickler.save(self, obj, save_persistent_id)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 556, in save
    f(self, obj)  # Call unbound method with explicit self
    ~^^^^^^^^^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 1217, in save_module_dict
    StockPickler.save_dict(pickler, obj)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 968, in save_dict
    self._batch_setitems(obj.items())
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 992, in _batch_setitems
    save(v)
    ~~~~^^^
  File "/Users/mmckerns/lib/python3.13/site-packages/dill/_dill.py", line 414, in save
    StockPickler.save(self, obj, save_persistent_id)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/pickle.py", line 574, in save
    rv = reduce(self.proto)
         ~~~~~~^^^^^^^^^^^^
TypeError: cannot pickle '_thread._ThreadHandle' object
>>> t.__dict__
{'_name': 'Thread-1', '_daemonic': False, '_ident': 123145514024960, '_native_id': 9483263, '_tstate_lock': <unlocked _thread.lock object at 0x1107a61c0>, '_handle': <_thread._ThreadHandle object: ident=123145514024960>, '_started': <threading.Event at 0x10fdfdf10: set>, '_is_stopped': False, '_initialized': True, '_stderr': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, '_invoke_excepthook': <function _make_invoke_excepthook.<locals>.invoke_excepthook at 0x110772ca0>}
>>> t._handle = None
>>> pt = dill.dumps(t)
>>> _t = dill.loads(pt)
>>>

while it's not directly related, might be an opportunity to deal with objects in #334

The current failure on Python 3.13.0b1 looks like this:

$ gh repo clone uqfoundation/dill
$ cd dill
$ python3.13 -V
Python 3.13.0b1
$ tox -e py313
.pkg: _optional_hooks> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: get_requires_for_build_sdist> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: get_requires_for_build_wheel> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: prepare_metadata_for_build_wheel> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: build_sdist> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True setuptools.build_meta
py313: install_package> python -I -m pip install --force-reinstall --no-deps /home/ben/src/forks/dill/.tox/.tmp/package/2/dill-0.3.9.dev0.tar.gz
py313: commands[0]> .tox/py313/bin/python -m pip install .
Processing /home/ben/src/forks/dill
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Installing backend dependencies ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: dill
  Building wheel for dill (pyproject.toml) ... done
  Created wheel for dill: filename=dill-0.3.9.dev0-py3-none-any.whl size=116162 sha256=86a3739b5a701f9aefd7840a31f695eefa0e8408a55028bfe497423a5d87d061
  Stored in directory: /tmp/pip-ephem-wheel-cache-sukup0uc/wheels/4b/69/4e/74fd16e96eb0e05af8c484f673696525e999353b5fec3ad50d
Successfully built dill
Installing collected packages: dill
  Attempting uninstall: dill
    Found existing installation: dill 0.3.9.dev0
    Uninstalling dill-0.3.9.dev0:
      Successfully uninstalled dill-0.3.9.dev0
Successfully installed dill-0.3.9.dev0
py313: commands[1]> .tox/py313/bin/python dill/tests/__main__.py
.<function <lambda> at 0x7fecfbe8be20>
<function <lambda> at 0x7f937fe8be20>
<function <lambda> at 0x7fed0408be20>
<function <lambda> at 0x7fbd8328be20>
<function <lambda> at 0x7f2a7ea8be20>
Traceback (most recent call last):
  File "/home/ben/src/forks/dill/dill/tests/test_detect.py", line 154, in <module>
    test_bad_things()
    ~~~~~~~~~~~~~~~^^
  File "/home/ben/src/forks/dill/dill/tests/test_detect.py", line 32, in test_bad_things
    assert len(s) is len(a) # TypeError (and possibly PicklingError)
           ^^^^^^^^^^^^^^^^
AssertionError
...F.......................
py313: exit 1 (1.79 seconds) /home/ben/src/forks/dill> .tox/py313/bin/python dill/tests/__main__.py pid=182079
  py313: FAIL code 1 (6.48=setup[2.72]+cmd[1.97,1.79] seconds)
  evaluation failed :( (6.53 seconds)

s is:

{('TypeError', "cannot pickle 'frame' object"), ('TypeError', "cannot pickle 'FrameLocalsProxy' object")}

and a is:

{'TypeError': "cannot pickle 'FrameLocalsProxy' object"}

which appears related in some way to PEP 667.

If I remember correctly -- from more than a decade ago -- the primary reason that a frame object couldn't be pickled is that the GIL is imbedded in the frame object and could only be written to in C. In python 3.13, there have been a lot of recent changes to support removal of the GIL. I expect it's related to that.