Skip to content

fix: restore pickling of FrozenList in the C extension - #835

Open
Mukller wants to merge 6 commits into
aio-libs:masterfrom
Mukller:fix/cext-pickling
Open

fix: restore pickling of FrozenList in the C extension#835
Mukller wants to merge 6 commits into
aio-libs:masterfrom
Mukller:fix/cext-pickling

Conversation

@Mukller

@Mukller Mukller commented Aug 23, 2026

Copy link
Copy Markdown

Fixes #834

Root cause

_frozenlist.pyx stores the frozen flag as cdef atomic[bint] _frozen (free-threading support). Cython's auto-generated __reduce_cython__ cannot convert a libcpp.atomic.atomic[bint] member into a Python object, so pickle.dumps() on any FrozenList raises:

TypeError: self._frozen cannot be converted to a Python object for pickling

The pure-Python implementation pickles fine on the same interpreter — the two implementations diverged, and there were no pickle tests to notice.

Fix

An explicit __reduce__ on the cdef class serializes (items, frozen) through a module-level _unpickle_frozen_list(items, frozen) constructor. The atomic stays in place (no free-threading regression); only the serialization path changes.

Testing

  • Added TestPickleC / TestPicklePy covering frozen and unfrozen round-trips for both implementations.
  • Regression proof: before this change the C-implementation tests fail with exactly the reported TypeError; with it, the whole suite passes: pytest tests/ -q → 222 passed (was 112 passed / collection without the new cases; both implementations exercised via the existing mixin pattern).
  • deepcopy, equality and hash behavior unchanged.
  • Towncrier fragment included (CHANGES/834.bugfix.rst).

Comment thread tests/test_frozenlist.py Fixed
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

The PR is not yet safe to merge because extension subclasses can still lose instance state and can fail to unpickle when their constructors do not accept the base-class item argument.

The reducer now preserves dynamic class identity, but it serializes no subclass state and reconstructs through cls(items), so valid subclasses can either silently lose attributes or raise during pickle.loads.

Files Needing Attention: frozenlist/_frozenlist.pyx and tests/test_frozenlist.py

Reviews (4): Last reviewed commit: "style(pyx): satisfy cython-lint blank-li..." | Re-trigger Greptile

Comment thread frozenlist/_frozenlist.pyx Outdated
Comment thread tests/test_frozenlist.py Outdated
Comment thread CHANGES/834.bugfix.rst
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (3d5c629) to head (f7fa46a).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master      #835   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            2         2           
  Lines          383       402   +19     
  Branches         9        10    +1     
=========================================
+ Hits           383       402   +19     
Flag Coverage Δ
CI-GHA 100.00% <100.00%> (ø)
OS-Linux 100.00% <100.00%> (ø)
OS-Windows 100.00% <100.00%> (ø)
OS-macOS 100.00% <100.00%> (ø)
Py-3.10.11 100.00% <100.00%> (ø)
Py-3.10.20 ?
Py-3.10.21 100.00% <100.00%> (ø)
Py-3.11.15 ?
Py-3.11.16 100.00% <100.00%> (ø)
Py-3.11.9 100.00% <100.00%> (ø)
Py-3.12.10 100.00% <100.00%> (ø)
Py-3.12.13 ?
Py-3.12.14 100.00% <100.00%> (ø)
Py-3.13.14 100.00% <100.00%> (ø)
Py-3.13.15 100.00% <100.00%> (ø)
Py-3.14.6 100.00% <100.00%> (ø)
Py-3.14.7 100.00% <100.00%> (ø)
Py-3.14.7t 100.00% <100.00%> (ø)
Py-pypy3.10.16-7.3.19 99.75% <100.00%> (+0.01%) ⬆️
VM-macos-latest 100.00% <100.00%> (ø)
VM-ubuntu-latest 100.00% <100.00%> (ø)
VM-windows-11-arm 100.00% <100.00%> (ø)
VM-windows-latest 100.00% <100.00%> (ø)
pytest 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The FrozenListMixin subclasses assign class-level aliases (FrozenList =
FrozenList / PyFrozenList) and subclass those aliases directly, which
mypy flags as [assignment]/[valid-type]/[type-arg]. The pattern is
intentional runtime dispatch, so annotate with targeted ignores instead
of restructuring. Fixes the red lint/Linter job on this PR.
@Mukller

Mukller commented Aug 23, 2026

Copy link
Copy Markdown
Author

The red lint / Linter job was my fault — mypy (run via the repo's pre-commit mypy hook) flagged 5 errors in tests/test_frozenlist.py: subclassing the PyFrozenList module-level alias and the mixin's class-attr alias assignments trip [valid-type] / [assignment] / [type-arg].

Pushed b956d6d with targeted # type: ignore[...] annotations on those lines plus a comment explaining that the alias-dispatch pattern is intentional runtime behavior the tests exercise. Local verification after the change:

  • pre-commit mypy hook: Passed (was: 5 errors)
  • pytest tests/test_frozenlist.py: 278 passed

Everything else in the run was already green (ruff check/format pass locally too).

# atomic[bint] `_frozen` member, so pickle the state explicitly.
return (
_unpickle_frozen_list,
(type(self), list(self._items), bool(self._frozen.load())),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Subclass state is discarded

When an extension-backed FrozenList subclass with instance attributes is pickled, this reducer serializes only its class, items, and frozen flag, so reconstruction creates a fresh subclass instance without restoring those attributes. The round-trip therefore silently loses subclass-specific state, unlike the pure-Python implementation.

Context Used: CLAUDE.md (source)

…zen_list

E303 (3 blank lines before the helper) and E305 (1 line after it) were
introduced together with the helper itself; normalize to 2/2.
@Mukller

Mukller commented Aug 24, 2026

Copy link
Copy Markdown
Author

Follow-up to the lint fix: CI also flagged cython-lint (E303/E305 around _unpickle_frozen_list — blank-line counts introduced together with the helper). Pushed f7fa46a normalizing spacing to 2/2.

Local verification on f7fa46a: full pre-commit run --all-files green (mypy + cython-lint included), pytest tests/test_frozenlist.py 278 passed.



def _unpickle_frozen_list(cls, items, frozen):
fl = cls(items)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Subclass constructor breaks unpickling

When an extension-backed FrozenList subclass defines an __init__ that does not accept one positional iterable, _unpickle_frozen_list calls cls(items), causing pickle.loads to raise TypeError; constructors with side effects are also executed again during reconstruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C extension: pickling any FrozenList raises TypeError (atomic[bint] not convertible)

2 participants