Skip to content

Fix copy.copy() sharing internal list between original and copy#743

Open
veeceey wants to merge 4 commits intoaio-libs:masterfrom
veeceey:fix-shallow-copy
Open

Fix copy.copy() sharing internal list between original and copy#743
veeceey wants to merge 4 commits intoaio-libs:masterfrom
veeceey:fix-shallow-copy

Conversation

@veeceey
Copy link
Copy Markdown

@veeceey veeceey commented Feb 10, 2026

Summary

  • copy.copy() on a FrozenList produced a broken shallow copy that shared the same underlying _items list with the original. Mutating either the original or the copy would affect both, violating the contract of copy.copy() for mutable containers.
  • Implemented __copy__ in both the Python and Cython (.pyx) implementations to create a properly independent shallow copy that preserves the frozen state.
  • Added the __copy__ signature to the .pyi type stubs.
  • Added 3 new tests covering unfrozen copy, frozen copy, and shallow-copy semantics (inner objects shared, container independent).

Details

Bug reproduction (before this fix):

from copy import copy
from frozenlist import FrozenList

fl = FrozenList([1, 2, 3])
c = copy(fl)
fl.append(4)
print(c)  # <FrozenList(frozen=False, [1, 2, 3, 4])>  -- BUG: copy was mutated!

Expected behavior (after this fix):

fl = FrozenList([1, 2, 3])
c = copy(fl)
fl.append(4)
print(c)  # <FrozenList(frozen=False, [1, 2, 3])>  -- copy is independent

The root cause is that FrozenList uses __slots__, so the default copy.copy() mechanism copies slot values by reference. Since _items is a list, both the original and the copy end up pointing to the exact same list object. The fix implements __copy__ which passes self._items to the constructor, which calls list(items) and thus creates a new list.

For comparison, copy.copy() on a standard list correctly produces an independent copy. The __deepcopy__ method was already implemented (PR #662), but __copy__ was missed.

Test plan

  • All existing tests continue to pass (both Python and C extension test classes)
  • New test_copy_unfrozen verifies that mutating the original does not affect the copy
  • New test_copy_frozen verifies frozen state is preserved in the copy
  • New test_copy_preserves_items verifies shallow-copy semantics (inner objects shared, but containers independent)

veeceey added a commit to veeceey/frozenlist that referenced this pull request Feb 10, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ce0bf30) to head (cabbfcf).

Additional details and impacted files
@@             Coverage Diff             @@
##           master      #743      +/-   ##
===========================================
+ Coverage   94.90%   100.00%   +5.09%     
===========================================
  Files          10         2       -8     
  Lines         726       370     -356     
  Branches       48        26      -22     
===========================================
- Hits          689       370     -319     
+ Misses         13         0      -13     
+ Partials       24         0      -24     
Flag Coverage Δ
CI-GHA 99.72% <100.00%> (+4.96%) ⬆️
MyPy ?
OS-Linux 99.72% <100.00%> (+0.02%) ⬆️
OS-Windows 99.72% <100.00%> (+0.02%) ⬆️
OS-macOS 99.72% <100.00%> (+0.02%) ⬆️
Py-3.10.11 99.72% <100.00%> (+0.02%) ⬆️
Py-3.10.19 99.72% <100.00%> (+0.02%) ⬆️
Py-3.11.14 99.72% <100.00%> (+0.02%) ⬆️
Py-3.11.9 99.72% <100.00%> (+0.02%) ⬆️
Py-3.12.10 99.72% <100.00%> (+0.02%) ⬆️
Py-3.12.12 99.72% <100.00%> (+0.02%) ⬆️
Py-3.13.10 ?
Py-3.13.10t ?
Py-3.13.11 99.72% <100.00%> (+0.02%) ⬆️
Py-3.13.11t ?
Py-3.13.12 99.72% <100.00%> (+0.02%) ⬆️
Py-3.13.12t 99.72% <100.00%> (+0.02%) ⬆️
Py-3.13.8 ?
Py-3.13.9 ?
Py-3.13.9t ?
Py-3.14.0 ?
Py-3.14.0t ?
Py-3.14.1 ?
Py-3.14.1t ?
Py-3.14.2 99.72% <100.00%> (+0.02%) ⬆️
Py-3.14.2t ?
Py-3.14.3 99.72% <100.00%> (+0.02%) ⬆️
Py-3.14.3t 99.72% <100.00%> (+0.02%) ⬆️
Py-3.9.13 99.72% <100.00%> (+0.02%) ⬆️
Py-3.9.25 99.72% <100.00%> (+0.02%) ⬆️
Py-pypy3.10.16-7.3.19 98.91% <100.00%> (+0.10%) ⬆️
Py-pypy3.9.19-7.3.16 98.91% <100.00%> (+0.10%) ⬆️
VM-macos-latest 99.72% <100.00%> (+0.02%) ⬆️
VM-ubuntu-latest 99.72% <100.00%> (+0.02%) ⬆️
VM-windows-11-arm 99.72% <100.00%> (+0.02%) ⬆️
VM-windows-latest 99.72% <100.00%> (+0.02%) ⬆️
pytest 99.72% <100.00%> (+0.02%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Feb 19, 2026

Friendly ping - any chance someone could take a look at this when they get a chance? Happy to make any changes if needed.

veeceey added 3 commits March 11, 2026 21:25
copy.copy() on a FrozenList produced a shallow copy that shared the
same underlying _items list object, causing mutations to one to affect
the other. This violates the contract of copy.copy() for mutable
containers (e.g. copy.copy() on a regular list correctly creates an
independent list).

Implement __copy__ in both the Python and Cython implementations to
create a new FrozenList with a copied _items list while preserving the
frozen state. Also add the method to the type stubs and add tests.
Python 3.14 added __annotations_cache__ to MutableSequence, which the
Cython extension class does not implement. Add it to the SKIP_METHODS
set alongside the other CPython internals already skipped there.
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.

1 participant