Skip to content

BUG: Make RangeIndex._data read-only to prevent external mutation - #67462

Open
Benlite777 wants to merge 3 commits into
pandas-dev:mainfrom
Benlite777:bugfix-rangeindex-readonly-49663
Open

BUG: Make RangeIndex._data read-only to prevent external mutation#67462
Benlite777 wants to merge 3 commits into
pandas-dev:mainfrom
Benlite777:bugfix-rangeindex-readonly-49663

Conversation

@Benlite777

@Benlite777 Benlite777 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

What does this fix

When a Series is created from a RangeIndex, the underlying cached _data array can be modified externally, silently corrupting the index. This happens because _data is decorated with @cache_readonly but the returned numpy array is writeable.

import pandas as pd

idx = pd.RangeIndex(3)
ser = pd.Series(idx)
ser.iloc[0] = 99
# idx is now corrupted: RangeIndex(start=0, stop=3, step=1) but _data shows [99, 1, 2]

Fix

Mark the _data array as read-only (data.flags.writeable = False) after construction, consistent with the same pattern used in:

  • DatetimeIndex (pandas/core/indexes/datetimelike.py:1041)
  • MultiIndex (pandas/core/indexes/multi.py:5133)
  • Index.values (pandas/core/indexes/base.py:5256)

Confirmed by @jbrockmendel that read-only is the desired approach.

Test

Added test_rangeindex_data_is_immutable which verifies:

  1. Direct modification of _data raises ValueError
  2. Creating a Series from a RangeIndex and modifying it does not corrupt the original index

Retargeted from #67060 per @rhshadrach's comment.

Fixes #67060

@rhshadrach

rhshadrach commented Aug 30, 2026

Copy link
Copy Markdown
Member

Apologies @Benlite777 - I didn't triage the original issue deep enough. #49663 already raises on main due to being read-only and should have been closed. Your issue is different. For yours, it's due to RangeIndex._simple_new setting _references=None.

Instead, I believe we need to create a new BlockValueRefs a RangeIndex (lazily, for perf) - you can do this by changing _references to being a @cache_readonly property on RangeIndex, something like:

@cache_readonly
def _references(self) -> BlockValuesRefs:  # type: ignore[override]
    result = BlockValuesRefs()
    result.add_index_reference(self)
    return result

cc @mroeschke for feedback on this proposal.

@rhshadrach rhshadrach left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Will also need to rework the whatsnew slightly too.

tm.assert_numpy_array_equal(result, expected)


def test_rangeindex_data_is_immutable():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With my comment above, this test can be removed and I think you can just add RangeIndex to the parametrization of test_series_from_index

@Benlite777

Copy link
Copy Markdown
Contributor Author

@rhshadrach Thanks for the review! I've reworked the fix based on your feedback:

Changes:

  • Reverted the _data writeable flag change (not needed, Index.values already handles that)
  • Fixed RangeIndex._simple_new to initialize _references with BlockValuesRefs() and register the index reference via add_index_reference, matching the pattern in Index._simple_new
  • Updated the test to verify that modifying a Series created from a RangeIndex does not corrupt the original index (CoW triggers a copy)
  • Reworked the whatsnew entry accordingly

@Benlite777
Benlite777 force-pushed the bugfix-rangeindex-readonly-49663 branch from 1992df1 to be6961d Compare August 31, 2026 06:25
@Benlite777

Copy link
Copy Markdown
Contributor Author

@rhshadrach Thanks for the detailed feedback! I've reworked the fix:

Changes:

  • Made _references a @cache_readonly property on RangeIndex (lazy initialization, as you suggested) instead of setting it in _simple_new
  • Removed _references = None from _simple_new — the property handles it now
  • Removed the standalone test_rangeindex_data_is_immutable test and added RangeIndex to the parametrization of test_series_from_index in test_constructors.py
  • Import for RangeIndex added to the test file

Waiting on @mroeschke's feedback on the approach as you mentioned.

RangeIndex._simple_new set _references=None, bypassing Copy-on-Write
reference tracking. This allowed a Series created from a RangeIndex
to silently corrupt the index when modified, since no CoW copy was
triggered.

Add a @cache_readonly _references property on RangeIndex that lazily
creates a BlockValuesRefs and registers the index reference. This
avoids the upfront cost in _simple_new while ensuring CoW works
correctly when the reference is actually needed.
@Benlite777
Benlite777 force-pushed the bugfix-rangeindex-readonly-49663 branch from 89f98b6 to b475696 Compare August 31, 2026 06:42
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.

2 participants