Skip to content

fix: make Version pickle-safe and backward-compatible with pre-26.1 pickles#1163

Merged
henryiii merged 10 commits intopypa:mainfrom
eachimei:pickle-compat
Apr 16, 2026
Merged

fix: make Version pickle-safe and backward-compatible with pre-26.1 pickles#1163
henryiii merged 10 commits intopypa:mainfrom
eachimei:pickle-compat

Conversation

@eachimei
Copy link
Copy Markdown
Contributor

Summary

Fixes #1162Version objects pickled with packaging < 26.1 fail to unpickle after upgrading, because the removed _structures.py module is referenced in the pickle stream.

Changes

This PR is structured as two independent commits so that commit 1 can land on its own if maintaining backward compatibility for old pickles (commit 2) is not desired.

Commit 1: Forward-fix — __reduce__

  • Adds __reduce__ to Version, serializing as (Version, ("1.2.3",)) — string-based reconstruction via __init__, never persisting internal caches or private types.
  • Adds parametrized test_pickle_roundtrip covering simple, pre-release, post-release, dev, epoch, and local versions.
  • Adds changelog entry under unreleased / Fixes.

Commit 2: Backward-fix — _structures.py shim + __setstate__

  • Restores a minimal _structures.py shim defining InfinityType and NegativeInfinityType — just enough to be unpicklable, not functional for comparisons. No import-time warning (would conflict with filterwarnings = ["error"] in pytest config).
  • Adds __setstate__ to Version that handles both old dict-based state (25.x) and tuple-based __slots__ state (26.0), discarding _key_cache and _hash_cache so they recompute with the current _cmpkey.
  • Tests use real pickle fixtures generated with packaging==25.0 and packaging==26.0 (with hash(v) called to force _key_cache population embedding InfinityType/NegativeInfinityType references).
  • Includes a re-pickle cleanliness test verifying that loading an old pickle and re-pickling produces a clean __reduce__-based pickle with no _structures references.

Testing

  • All 51,511 existing tests pass (python -m pytest tests/test_version.py -x)
  • 4 new test functions added, all passing

Implement __reduce__ on Version so it pickles as (Version, ('1.2.3',)),
always reconstructing via __init__. This prevents internal types and
cached values from leaking into the pickle stream.

Add parametrized pickle round-trip tests covering simple, pre-release,
post-release, dev, epoch, and local version strings.
Add a minimal _structures.py shim that provides InfinityType and
NegativeInfinityType  the exact class names referenced in pickles
created with packaging <= 25.x.

Implement __setstate__ on Version to restore only core version fields
and discard the stale _key cache, which may contain old _structures
objects. Handles both the old dict format (packaging <= 25.x) and the
new __slots__ tuple format (packaging 26.x).

Add tests using real pickle bytes generated with packaging 25.0 that
verify old pickles load correctly, produce correct comparisons, and
re-pickle cleanly without _structures references.
Comment thread tests/test_version.py Outdated
Comment thread src/packaging/version.py Outdated
@notatallshaw
Copy link
Copy Markdown
Member

Won't have time to review this until the evening, but for performance could we serialize as parts and reconstruct via from parts?

@henryiii
Copy link
Copy Markdown
Contributor

We should also add a 26.2 pickle (current state) to protect for the future.

@henryiii
Copy link
Copy Markdown
Contributor

Yes, if we care about performance, a getstate/setstate pair could serialize to/from parts. Maybe we could even mimic the 26.1 shape to reduce the number of different shapes to handle down to 2. But I'm not sure how the performance compares to pickling itself, it might not be that much compared to the pickle process itself?

@eachimei
Copy link
Copy Markdown
Contributor Author

We should also add a 26.2 pickle (current state) to protect for the future.

Done in ba06290

@eachimei
Copy link
Copy Markdown
Contributor Author

Won't have time to review this until the evening, but for performance could we serialize as parts and reconstruct via from parts?

Please see #1163 (comment)

@eachimei
Copy link
Copy Markdown
Contributor Author

Addressing now the missing unit-test coverage from previous build...

@eachimei
Copy link
Copy Markdown
Contributor Author

Addressing now the missing unit-test coverage from previous build...

Done

@eachimei eachimei requested a review from henryiii April 16, 2026 15:44
Comment thread src/packaging/version.py Outdated
Comment thread CHANGELOG.rst Outdated
@eachimei eachimei requested a review from henryiii April 16, 2026 16:04
@henryiii henryiii changed the title Make Version pickle-safe and backward-compatible with pre-26.1 pickles fix: make Version pickle-safe and backward-compatible with pre-26.1 pickles Apr 16, 2026
Copy link
Copy Markdown
Contributor

@henryiii henryiii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, will wait for @notatallshaw to take a look too.

@notatallshaw
Copy link
Copy Markdown
Member

I'm happy for this to be merged, I won't be able to thoroughly review until at least late this night, and I don't want to be a blocker, at a skim this looks good.

@henryiii henryiii merged commit b82413d into pypa:main Apr 16, 2026
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Version objects pickled before 26.1 fail to unpickle (ModuleNotFoundError: packaging._structures)

3 participants