Repository navigation
Match upstream cloudpickle in the behaviors found by a line-by-line audit - #3
Merged
Merged
Conversation
…udit A line-by-line review of upstream's cloudpickle/cloudpickle.py against the Rust implementation found no missing function or code path, but found these differences, now fixed: - Upstream's functions read the globals of their module and the attributes of the standard library modules they use at call time, so replacing them (e.g. with monkeypatch) changes their behavior. Each Rust function now checks that the globals read by the upstream function still hold their initial values, and otherwise runs the upstream function, compiled from a bundled copy of upstream's cloudpickle.py (src/upstream/cloudpickle.py) with the namespace of cloudpickle_rust.cloudpickle as its globals. - The functions of the module and the methods of Pickler (except reducer_override, which the C pickler calls for every object) are Python functions calling their Rust implementation, with upstream's signatures, docstrings and globals: like upstream's, they are types.FunctionType instances (Pickler subclasses see the same _function_reduce calls) and warnings raised while pickling are attributed to the module. - _extract_code_globals_cache is a weakref.WeakKeyDictionary and _empty_cell_value an instance of a Python class, like upstream's. - Same error messages as upstream for _getattribute on Python 3.12/3.13, for iterable unpacking on Python 3.14, for file objects with a non-string mode or name, and in register/unregister_pickle_by_value (format()). - _walk_global_ops, _extract_code_globals and _find_imported_submodules handle other objects than code objects like upstream. - On PyPy, the functions of the module are saved by save_function like upstream's (the extra reducer_override is removed). Pickling functions and classes is about 10% slower than with 0.1.0 (the cost of these semantics); the README benchmark is updated. New tests compare, with upstream cloudpickle, the behavior when each global of the module or each imported module is replaced, the functions (signatures, docstrings, globals), the error messages and the bundled source. Co-authored-by: Isaac <no-reply@databricks.com>
Windows formats the addresses in the repr of objects with uppercase hexadecimal digits. Co-authored-by: Isaac <no-reply@databricks.com>
dev/generate_upstream_docs.py extracts the docstrings of the functions and of the Pickler methods from src/upstream/cloudpickle.py with the ast module (the values of their literals, whatever the version of Python). Co-authored-by: Isaac <no-reply@databricks.com>
HyukjinKwon
force-pushed
the
audit-upstream-parity
branch
from
November 5, 2026 09:30
beed6a3 to
2bc066f
Compare
This was referenced Nov 5, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
I compared every line of upstream's
cloudpickle/cloudpickle.py(commit4af5936) with the Rust implementation. No function, branch or payload difference was missing. This PR fixes the behavior differences the comparison found.Globals are read at call time, like upstream
Upstream's functions read their module's globals on every call: its state (
_PICKLE_BY_VALUE_MODULES, the class trackers,_extract_code_globals_cache, …), its own functions, and the modules it imported. They also read the standard-library attributes they use (types.FunctionType,uuid.uuid4,sys.modules,pickle.PicklingError,warnings.warn, …). Replacing one of them, for example withmonkeypatch, therefore changes upstream's behavior. The port bound them at import, so the same replacement had no effect.Now each Rust function first checks that the globals the upstream function reads still hold their initial values, at one dictionary lookup each. If one was replaced, the call runs the upstream function itself. It is compiled on demand from a byte-identical copy of upstream's
cloudpickle.py(src/upstream/cloudpickle.py), with the namespace ofcloudpickle_rust.cloudpickleas its globals (src/fallback.rs). The per-function dependency lists come from upstream's bytecode._code_reducereads a single global,types.CodeType, when it returns. It looks it up at that point instead of running upstream's code, soDYNAMIC_CODE_FILENAME(#2) applies in every case.The functions are Python functions, like upstream's
The module's 65 functions and the
Picklermethods are now Python functions that call their Rust implementation (src/functions.rs). They have upstream's signatures (now including_make_cell(value=_empty_cell_value)), docstrings,__qualname__,__module__and__globals__. The docstrings are compiled by the running interpreter, so they also match Python 3.13's docstring dedenting. They come fromsrc/upstream_docs.rs, whichdev/generate_upstream_docs.pygenerates from the bundled source. As a result:isinstance(f, types.FunctionType)matches upstream. The pickler therefore routes the module's own reconstructors through_function_reduce, soPicklersubclasses and replaced_should_pickle_by_referencesee the same calls as upstream.cloudpickle.cloudpickle.The exception is
Pickler.reducer_override: the C pickler calls it for every object, so it stays a native method descriptor.Other fixes
_extract_code_globals_cacheis aweakref.WeakKeyDictionary, and_empty_cell_valueis an instance of a Python class, both like upstream's._getattributeon 3.12 and 3.13;_function_setstateand_class_setstateon 3.14;_file_reducefor a non-stringmodeorname(%formatting);register_pickle_by_valueandunregister_pickle_by_value(format())._walk_global_ops,_extract_code_globalsand_find_imported_submodulesaccept the same non-code inputs as upstream. For example,_walk_global_ops(func)returns upstream's lazy generator.save_function, and the extrareducer_overridethe port added is removed.LICENSEis updated. TheCHANGES.mdentries go in the 0.2.0 section, next to Add DYNAMIC_CODE_FILENAME to keep the filenames of the code pickled by value #2's. The README benchmark table is updated too: pickling functions is about 10% slower than in 0.1.0, which is the cost of these semantics, while instance-heavy data is unchanged.Not ported: upstream's label-triggered downstream CI jobs (distributed, joblib, loky, ray). Its
MANIFEST.in,.git-blame-ignore-revsanddev/make-distribution.sharen't needed here.Verification
Rebased on main after #2 was merged (conflicts resolved:
module_globalreplaced byfallback::load_global).test_walk_global_ops_matches_dis, which now passes on 7.3.23 where the old baseline failed.-D warnings, and the sdist (which includes the new sources).This pull request and its description were written by Isaac.