Skip to content

Guard CoW read-only .to_numpy()/.values mutations under pandas 3 - #3445

Merged
deruyter92 merged 9 commits into
DeepLabCut:devfrom
AxelNoun:fix/pandas3-cow-guards
Aug 31, 2026
Merged

deruyter92 merged 9 commits into
DeepLabCut:devfrom
AxelNoun:fix/pandas3-cow-guards

Conversation

@AxelNoun

Copy link
Copy Markdown
Contributor

Recreates #3416, which was closed automatically on 12 Aug when the fork
hosting its branch was detached during an account cleanup on my side —
my mistake, apologies for the noise.

The head commit is byte-identical to the original PR (a3977ff926), so it
still includes your own commits on that branch, @deruyter92 — nothing was
rebased or dropped. The original review thread remains readable at
#3416, where the PR was approved.


Guard in-place mutations of .to_numpy() / .values results (Copy-on-Write, pandas 3.0)

Follow-up to #3360. Related to the tracking issue #3362.

Summary

Under pandas 3.0's Copy-on-Write, .to_numpy() and .values return a
read-only array for single-dtype selections; mutating that array in place
raises ValueError: assignment destination is read-only. This PR guards the five
sites where such an array is mutated, using to_numpy(copy=True).

The pandas<3 upper bound is intentionally left unchanged — this is a
forward-compatibility fix, not the pin removal discussed in #3362. It complements
the preparatory work already merged in #3360 and is safe on the currently pinned
pandas 2.x.

The pattern

arr = df.to_numpy()      # or df.values -> read-only view under CoW (single dtype)
arr[mask] = value        # ValueError: assignment destination is read-only

copy=True returns a writable array and is behavior-preserving on pandas 2.x, so
the change is correct under both major versions.

Changes

to_numpy(copy=True) at the sites that mutate the extracted array in place:

  • pose_estimation_pytorch/data/ctd.py
  • pose_estimation_3d/triangulation.py
  • pose_estimation_3d/plotting3D.py
  • refine_training_dataset/tracklets.py
  • post_processing/filtering.py — spline-filter branch (same interpolation
    pattern as tracklets.py; only the tracklets copy is covered by the current
    tests, so this one was located by a static scan rather than a failing test)

Considered and left unchanged: pose_estimation_tensorflow/core/evaluate_multianimal.py
extracts a mixed-dtype selection (["sample", "y", "x", "bodyparts"]), which
yields a writable object array and is therefore not affected.

Testing

  • Locally against pandas 3.0.3 + PyTables 3.11.1: the CTD-HDF tests pass after
    the fix, and the spline-filter path is smoke-tested (writable arrays, correct
    gap-filling).
  • CI continues to run on pandas 2.x (unchanged pin), where these changes are
    behavior-preserving.

Notes for #3362

  • This is a class of pandas-3 break not covered by Prepare migration to pandas 3.0 #3360 — it only surfaces at
    runtime under pandas 3.0, so it isn't caught by the 2.3 future-mode tooling.
  • While validating against released pandas 3.0.3, the HDF concern described in
    Migration to Pandas 3.0 #3362 ("PyTables cannot serialize a MultiIndex whose levels use extension
    dtypes -> change all dataframes back to object before saving"
    ) does not
    reproduce: pandas 3.0 explicitly whitelists StringDtype in that guard
    (io/pytables.py, write_multi_index:
    isinstance(lev.dtype, ExtensionDtype) and not isinstance(lev.dtype, StringDtype)),
    and DeepLabCut's string index/column levels serialize fine (verified for the
    df_with_missing / tracks / predictions / ass keys). So no to_hdf
    object-conversion wrapper appears necessary on the released 3.0. (The pandas 2.3
    future.infer_string mode does raise there, since the exemption only lands in
    3.0 — so that mode shouldn't be used to validate HDF writes.)

AxelNoun and others added 9 commits July 19, 2026 19:04
Under pandas 3.0 Copy-on-Write, .to_numpy()/.values return read-only
arrays for single-dtype selections; in-place mutation raises
ValueError. Use copy=True at the five affected sites. Pin unchanged.
Follow-up to the pandas 3.0 CoW sweep (DeepLabCut#3362). Three more sites extract a
NumPy array from a pandas object and mutate it in place; under pandas 3 CoW
these can be read-only views, raising "assignment destination is read-only".

- refine_training_dataset/tracklets.py: self.data backs self.xy/self.prob,
  which swap_tracklets and the refine GUI mutate in place (HDF load path;
  flagged in review).
- utils/make_labeled_video.py: coords is a view into xyp, masked in place in
  both the first-frame and per-frame branches.
- pose_estimation_tensorflow/.../pose_multianimal_imgaug.py: a single-row
  float Series is read-only; kpts is masked in place when
  mask_kpts_below_thresh is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
Guards against the in-place write on the next line producing a
read-only view when the intermediate frame becomes single-block.
@C-Achard C-Achard added the dependencies Pull requests that update a dependency file label Aug 13, 2026
@C-Achard
C-Achard self-requested a review August 13, 2026 08:09

@C-Achard C-Achard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@deruyter92 As mentioned, PR is identical to previously, so I'm re-approving for convenience

@deruyter92 deruyter92 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@AxelNoun thanks for re-opening. Again, great PR, thanks for your contribution!

@AxelNoun

Copy link
Copy Markdown
Contributor Author

@AxelNoun thanks for re-opening. Again, great PR, thanks for your contribution!

Thank you for the kind words! I ran into some issues managing my fork, which caused my previous PRs to be closed. I'm sorry for the inconvenience, and I'll make sure it doesn't happen again. Thanks a lot for taking the time to re-review this one!

@deruyter92 deruyter92 added the 3.1 label Aug 28, 2026
@deruyter92
deruyter92 changed the base branch from main to dev August 31, 2026 11:55
@deruyter92
deruyter92 merged commit c6ad634 into DeepLabCut:dev Aug 31, 2026
60 checks passed
@deruyter92 deruyter92 added 3.0.2 and removed 3.1 labels Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.0.2 dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants