Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0537d03
TST: add regression tests for GH#63388 (DatetimeIndex copy behavior)
zacharym-collins Dec 17, 2025
3b7b85b
ENH: Copy inputs in DatetimeIndex constructor by default (GH#63388)
zacharym-collins Dec 17, 2025
67eaf05
CLN: change test name to include datetimeindex for clarity
zacharym-collins Dec 17, 2025
f59ab9e
TST: Add regression tests for GH#63388 (TimedeltaIndex copy behavior)
zacharym-collins Dec 17, 2025
9221e5a
ENH: Copy inputs in TimedeltaIndex constructor by default (GH#63388)
zacharym-collins Dec 17, 2025
32ee4dc
TST: correct test to use array instead of np.array
zacharym-collins Dec 17, 2025
b55f4a0
TST: correct test to use array instead of np.array
zacharym-collins Dec 17, 2025
91e78da
TST: Add regression tests for GH#63388 (PeriodIndex copy behavior)
zacharym-collins Dec 17, 2025
1252782
ENH: Copy inputs in PeriodIndex constructor by default (GH#63388)
zacharym-collins Dec 17, 2025
923c949
TST: Add regression tests for GH#63388 (IntervalIndex copy behavior)
zacharym-collins Dec 17, 2025
122f554
ENH: Copy inputs in IntervalIndex constructor by default (GH#63388)
zacharym-collins Dec 17, 2025
66a37fe
STYLE: Apply isort fixes
zacharym-collins Dec 17, 2025
5a211cc
DOC: Add release note for GH#63388
zacharym-collins Dec 17, 2025
9373333
TYP: Fix mypy errors with type ignores
zacharym-collins Dec 17, 2025
bc3c54a
TYP: Fix mypy errors using bool(copy) and ignores
zacharym-collins Dec 17, 2025
6731252
STY: Apply pre-commit fixes
zacharym-collins Dec 17, 2025
9acc8d1
FIX: Update regression test for copy default
zacharym-collins Dec 17, 2025
180fa7e
FIX: handle string dtypes
zacharym-collins Dec 17, 2025
f7a00a4
FIX: Update test_array_tz to use copy=False
zacharym-collins Dec 17, 2025
ca90c29
STY: apply pre-commit fixes
zacharym-collins Dec 17, 2025
75a3aac
CLN: Remove unused type ignores
zacharym-collins Dec 17, 2025
e689f7b
REF: Add Index._maybe_copy_input helper (GH#63388)
zacharym-collins Dec 17, 2025
65d8012
REF: Use _maybe_copy_input in Index subclasses (GH#63388)
zacharym-collins Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ Other API changes
:meth:`~DataFrame.ffill`, :meth:`~DataFrame.bfill`, :meth:`~DataFrame.interpolate`,
:meth:`~DataFrame.where`, :meth:`~DataFrame.mask`, :meth:`~DataFrame.clip`) now return
the modified DataFrame or Series (``self``) instead of ``None`` when ``inplace=True`` (:issue:`63207`)
- :class:`DatetimeIndex`, :class:`TimedeltaIndex`, :class:`PeriodIndex` and :class:`IntervalIndex` constructors now copy the input ``data`` by default when ``copy=None``, consistent with :class:`Index` behavior (:issue:`63388`)
Copy link
Member

Choose a reason for hiding this comment

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

Could you remove this note (I'll probably be adding something related to this in the copy on write guide)

Copy link
Member

Choose a reason for hiding this comment

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

I think it is probably still worth mentioning it here explicitly as well? But I think we can simplify this to just say that all the Index constructors now copy array input by default, to be consistent with Series

Copy link
Member

Choose a reason for hiding this comment

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

OK sure sounds good. @zacharym-collins could you apply that suggestion. Also could you clarify that only numpy arrays and pandas ExtensionArrays are copied by default?


.. ---------------------------------------------------------------------------
.. _whatsnew_300.deprecations:
Expand Down
13 changes: 13 additions & 0 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5196,6 +5196,19 @@ def _raise_scalar_data_error(cls, data):
"was passed"
)

@classmethod
def _maybe_copy_input(cls, data, copy, dtype):
Copy link
Member

@mroeschke mroeschke Dec 17, 2025

Choose a reason for hiding this comment

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

Suggested change
def _maybe_copy_input(cls, data, copy, dtype):
def _maybe_copy_array_input(cls, data, copy: bool | None, dtype) -> tuple[Any, bool]:

"""
Ensure that the input data is copied if necessary.
GH#63388
"""
if isinstance(data, (ExtensionArray, np.ndarray)):
if copy is not False:
if dtype is None or astype_is_view(data.dtype, pandas_dtype(dtype)):
data = data.copy()
copy = False
return data, copy
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return data, copy
return data, bool(copy)

Then you can remove the bool(copy) in other files


def _validate_fill_value(self, value):
"""
Check if the value can be inserted into our array without casting,
Expand Down
20 changes: 15 additions & 5 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
)
from pandas.util._exceptions import find_stack_level

from pandas.core.dtypes.common import is_scalar
from pandas.core.dtypes.common import (
is_scalar,
)
from pandas.core.dtypes.dtypes import (
ArrowDtype,
DatetimeTZDtype,
Expand Down Expand Up @@ -181,8 +183,13 @@ class DatetimeIndex(DatetimeTimedeltaMixin):
If True parse dates in `data` with the year first order.
dtype : numpy.dtype or DatetimeTZDtype or str, default None
Note that the only NumPy dtype allowed is `datetime64[ns]`.
copy : bool, default False
Make a copy of input ndarray.
copy : bool, default None
Whether to copy input data, only relevant for array, Series, and Index
inputs (for other input, e.g. a list, a new array is created anyway).
Defaults to True for array input and False for Index/Series.
Set to False to avoid copying array input at your own risk (if you
know the input data won't be modified elsewhere).
Set to True to force copying Series/Index up front.
name : label, default None
Name to be stored in the index.
Expand Down Expand Up @@ -669,7 +676,7 @@ def __new__(
dayfirst: bool = False,
yearfirst: bool = False,
dtype: Dtype | None = None,
copy: bool = False,
copy: bool | None = None,
name: Hashable | None = None,
) -> Self:
if is_scalar(data):
Expand All @@ -679,6 +686,9 @@ def __new__(

name = maybe_extract_name(name, data, cls)

# GH#63388
data, copy = cls._maybe_copy_input(data, copy, dtype)

if (
isinstance(data, DatetimeArray)
and freq is lib.no_default
Expand All @@ -694,7 +704,7 @@ def __new__(
dtarr = DatetimeArray._from_sequence_not_strict(
data,
dtype=dtype,
copy=copy,
copy=bool(copy),
tz=tz,
freq=freq,
dayfirst=dayfirst,
Expand Down
16 changes: 12 additions & 4 deletions pandas/core/indexes/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,13 @@ class IntervalIndex(ExtensionIndex):
neither.
dtype : dtype or None, default None
If None, dtype will be inferred.
copy : bool, default False
Copy the input data.
copy : bool, default None
Whether to copy input data, only relevant for array, Series, and Index
inputs (for other input, e.g. a list, a new array is created anyway).
Defaults to True for array input and False for Index/Series.
Set to False to avoid copying array input at your own risk (if you
know the input data won't be modified elsewhere).
Set to True to force copying Series/Index input up front.
name : object, optional
Name to be stored in the index.
verify_integrity : bool, default True
Expand Down Expand Up @@ -252,17 +257,20 @@ def __new__(
data,
closed: IntervalClosedType | None = None,
dtype: Dtype | None = None,
copy: bool = False,
copy: bool | None = None,
name: Hashable | None = None,
verify_integrity: bool = True,
) -> Self:
name = maybe_extract_name(name, data, cls)

# GH#63388
data, copy = cls._maybe_copy_input(data, copy, dtype)

with rewrite_exception("IntervalArray", cls.__name__):
array = IntervalArray(
data,
closed=closed,
copy=copy,
copy=bool(copy),
dtype=dtype,
verify_integrity=verify_integrity,
)
Expand Down
18 changes: 14 additions & 4 deletions pandas/core/indexes/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
set_module,
)

from pandas.core.dtypes.common import is_integer
from pandas.core.dtypes.common import (
is_integer,
)
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

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

Can you undo these import changes if the contents haven't changed in your PR?

from pandas.core.dtypes.dtypes import PeriodDtype
from pandas.core.dtypes.generic import ABCSeries
from pandas.core.dtypes.missing import is_valid_na_for_dtype
Expand Down Expand Up @@ -101,8 +103,13 @@ class PeriodIndex(DatetimeIndexOpsMixin):
One of pandas period strings or corresponding objects.
dtype : str or PeriodDtype, default None
A dtype from which to extract a freq.
copy : bool
Make a copy of input ndarray.
copy : bool, default None
Whether to copy input data, only relevant for array, Series, and Index
inputs (for other input, e.g. a list, a new array is created anyway).
Defaults to True for array input and False for Index/Series.
Set to False to avoid copying array input at your own risk (if you
know the input data won't be modified elsewhere).
Set to True to force copying Series/Index input up front.
name : str, default None
Name of the resulting PeriodIndex.
Expand Down Expand Up @@ -220,7 +227,7 @@ def __new__(
data=None,
freq=None,
dtype: Dtype | None = None,
copy: bool = False,
copy: bool | None = None,
name: Hashable | None = None,
) -> Self:
refs = None
Expand All @@ -231,6 +238,9 @@ def __new__(

freq = validate_dtype_freq(dtype, freq)

# GH#63388
data, copy = cls._maybe_copy_input(data, copy, dtype)

# PeriodIndex allow PeriodIndex(period_index, freq=different)
# Let's not encourage that kind of behavior in PeriodArray.

Expand Down
16 changes: 12 additions & 4 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,13 @@ class TimedeltaIndex(DatetimeTimedeltaMixin):
dtype : numpy.dtype or str, default None
Valid ``numpy`` dtypes are ``timedelta64[ns]``, ``timedelta64[us]``,
``timedelta64[ms]``, and ``timedelta64[s]``.
copy : bool
Make a copy of input array.
copy : bool, default None
Whether to copy input data, only relevant for array, Series, and Index
inputs (for other input, e.g. a list, a new array is created anyway).
Defaults to True for array input and False for Index/Series.
Set to False to avoid copying array input at your own risk (if you
know the input data won't be modified elsewhere).
Set to True to force copying Series/Index input up front.
name : object
Name to be stored in the index.

Expand Down Expand Up @@ -158,11 +163,14 @@ def __new__(
data=None,
freq=lib.no_default,
dtype=None,
copy: bool = False,
copy: bool | None = None,
name=None,
):
name = maybe_extract_name(name, data, cls)

# GH#63388
data, copy = cls._maybe_copy_input(data, copy, dtype)

if is_scalar(data):
cls._raise_scalar_data_error(data)

Expand Down Expand Up @@ -192,7 +200,7 @@ def __new__(
# - Cases checked above all return/raise before reaching here - #

tdarr = TimedeltaArray._from_sequence_not_strict(
data, freq=freq, unit=None, dtype=dtype, copy=copy
data, freq=freq, unit=None, dtype=dtype, copy=bool(copy)
)
refs = None
if not copy and isinstance(data, (ABCSeries, Index)):
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/arrays/test_datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ def test_array_object_dtype(self, arr1d):
def test_array_tz(self, arr1d):
# GH#23524
arr = arr1d
dti = self.index_cls(arr1d)
dti = self.index_cls(arr1d, copy=False)
copy_false = None if np_version_gt2 else False

expected = dti.asi8.view("M8[ns]")
Expand Down
30 changes: 30 additions & 0 deletions pandas/tests/copy_view/index/test_datetimeindex.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import numpy as np
import pytest

from pandas import (
DatetimeIndex,
Series,
Timestamp,
array,
date_range,
)
import pandas._testing as tm
from pandas.tests.copy_view.util import get_array

pytestmark = pytest.mark.filterwarnings(
"ignore:Setting a value on a view:FutureWarning"
Expand Down Expand Up @@ -54,3 +57,30 @@ def test_index_values():
idx = date_range("2019-12-31", periods=3, freq="D")
result = idx.values
assert result.flags.writeable is False


def test_constructor_copy_input_datetime_ndarray_default():
# GH 63388
arr = np.array(["2020-01-01", "2020-01-02"], dtype="datetime64[ns]")
idx = DatetimeIndex(arr)
assert not np.shares_memory(arr, get_array(idx))


def test_constructor_copy_input_datetime_ea_default():
# GH 63388
arr = array(["2020-01-01", "2020-01-02"], dtype="datetime64[ns]")
idx = DatetimeIndex(arr)
assert not tm.shares_memory(arr, idx.array)


def test_series_from_temporary_datetimeindex_readonly_data():
# GH 63388
arr = np.array(["2020-01-01", "2020-01-02"], dtype="datetime64[ns]")
arr.flags.writeable = False
ser = Series(DatetimeIndex(arr))
assert not np.shares_memory(arr, get_array(ser))
ser.iloc[0] = Timestamp("2020-01-01")
expected = Series(
[Timestamp("2020-01-01"), Timestamp("2020-01-02")], dtype="datetime64[ns]"
)
tm.assert_series_equal(ser, expected)
29 changes: 29 additions & 0 deletions pandas/tests/copy_view/index/test_intervalindex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import numpy as np

from pandas import (
Interval,
IntervalIndex,
Series,
array,
)
import pandas._testing as tm
from pandas.tests.copy_view.util import get_array


def test_constructor_copy_input_interval_ea_default():
# GH 63388
arr = array([Interval(0, 1), Interval(1, 2)])
idx = IntervalIndex(arr)
assert not tm.shares_memory(arr, idx.array)


def test_series_from_temporary_intervalindex_readonly_data():
# GH 63388
arr = array([Interval(0, 1), Interval(1, 2)])
arr._left.flags.writeable = False
arr._right.flags.writeable = False
ser = Series(IntervalIndex(arr))
assert not np.shares_memory(arr._left, get_array(ser)._left)
ser.iloc[0] = Interval(5, 6)
expected = Series([Interval(5, 6), Interval(1, 2)], dtype="interval[int64, right]")
tm.assert_series_equal(ser, expected)
24 changes: 24 additions & 0 deletions pandas/tests/copy_view/index/test_periodindex.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import numpy as np
import pytest

from pandas import (
Period,
PeriodIndex,
Series,
array,
period_range,
)
import pandas._testing as tm
from pandas.tests.copy_view.util import get_array

pytestmark = pytest.mark.filterwarnings(
"ignore:Setting a value on a view:FutureWarning"
Expand All @@ -21,3 +24,24 @@ def test_periodindex(box):
expected = idx.copy(deep=True)
ser.iloc[0] = Period("2020-12-31")
tm.assert_index_equal(idx, expected)


def test_constructor_copy_input_period_ea_default():
# GH 63388
arr = array(["2020-01-01", "2020-01-02"], dtype="period[D]")
idx = PeriodIndex(arr)
assert not tm.shares_memory(arr, idx.array)


def test_series_from_temporary_periodindex_readonly_data():
# GH 63388
arr = array(["2020-01-01", "2020-01-02"], dtype="period[D]")
arr._ndarray.flags.writeable = False
ser = Series(PeriodIndex(arr))
assert not np.shares_memory(arr._ndarray, get_array(ser))
ser.iloc[0] = Period("2022-01-01", freq="D")
expected = Series(
[Period("2022-01-01", freq="D"), Period("2020-01-02", freq="D")],
dtype="period[D]",
)
tm.assert_series_equal(ser, expected)
30 changes: 30 additions & 0 deletions pandas/tests/copy_view/index/test_timedeltaindex.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import numpy as np
import pytest

from pandas import (
Series,
Timedelta,
TimedeltaIndex,
array,
timedelta_range,
)
import pandas._testing as tm
from pandas.tests.copy_view.util import get_array

pytestmark = pytest.mark.filterwarnings(
"ignore:Setting a value on a view:FutureWarning"
Expand All @@ -27,3 +30,30 @@ def test_timedeltaindex(cons):
expected = idx.copy(deep=True)
ser.iloc[0] = Timedelta("5 days")
tm.assert_index_equal(idx, expected)


def test_constructor_copy_input_timedelta_ndarray_default():
# GH 63388
arr = np.array([1, 2], dtype="timedelta64[ns]")
idx = TimedeltaIndex(arr)
assert not np.shares_memory(arr, get_array(idx))


def test_constructor_copy_input_timedelta_ea_default():
# GH 63388
arr = array([1, 2], dtype="timedelta64[ns]")
idx = TimedeltaIndex(arr)
assert not tm.shares_memory(arr, idx.array)


def test_series_from_temporary_timedeltaindex_readonly_data():
# GH 63388
arr = np.array([1, 2], dtype="timedelta64[ns]")
arr.flags.writeable = False
ser = Series(TimedeltaIndex(arr))
assert not np.shares_memory(arr, get_array(ser))
ser.iloc[0] = Timedelta(days=1)
expected = Series(
[Timedelta(days=1), Timedelta(nanoseconds=2)], dtype="timedelta64[ns]"
)
tm.assert_series_equal(ser, expected)
2 changes: 1 addition & 1 deletion pandas/tests/indexes/datetimes/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ def test_index_cast_datetime64_other_units(self):
def test_constructor_int64_nocopy(self):
# GH#1624
arr = np.arange(1000, dtype=np.int64)
index = DatetimeIndex(arr)
index = DatetimeIndex(arr, copy=False)

arr[50:100] = -1
assert (index.asi8[50:100] == -1).all()
Expand Down
Loading