From 0537d03be6be3502aec8f051a29b88cf1c8fa264 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 06:36:50 -0600 Subject: [PATCH 01/30] TST: add regression tests for GH#63388 (DatetimeIndex copy behavior) --- .../copy_view/index/test_datetimeindex.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandas/tests/copy_view/index/test_datetimeindex.py b/pandas/tests/copy_view/index/test_datetimeindex.py index 6194ea8b122c9..6b289c856fd93 100644 --- a/pandas/tests/copy_view/index/test_datetimeindex.py +++ b/pandas/tests/copy_view/index/test_datetimeindex.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from pandas import ( @@ -7,6 +8,7 @@ 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" @@ -54,3 +56,31 @@ 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 = np.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_index_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) From 3b7b85b4e3bba9b4e71a4c204c261b9043abf60f Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:07:19 -0600 Subject: [PATCH 02/30] ENH: Copy inputs in DatetimeIndex constructor by default (GH#63388) --- pandas/core/indexes/datetimes.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index a07e18b1892fd..21944a0e62c79 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -42,6 +42,7 @@ ArrowDtype, DatetimeTZDtype, ) +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.generic import ABCSeries from pandas.core.dtypes.missing import is_valid_na_for_dtype @@ -49,6 +50,7 @@ DatetimeArray, tz_to_dtype, ) +from pandas.core.arrays import ExtensionArray import pandas.core.common as com from pandas.core.indexes.base import ( Index, @@ -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. @@ -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): @@ -679,6 +686,13 @@ def __new__( name = maybe_extract_name(name, data, cls) + if isinstance(data, (ExtensionArray, np.ndarray)): + # GH 63388 + if copy is not False: + if dtype is None or astype_is_view(data.dtype, dtype): + data = data.copy() + copy = False + if ( isinstance(data, DatetimeArray) and freq is lib.no_default From 67eaf0568b5769d915df9822249c6e2d69eed7ae Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:26:49 -0600 Subject: [PATCH 03/30] CLN: change test name to include datetimeindex for clarity --- pandas/tests/copy_view/index/test_datetimeindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/copy_view/index/test_datetimeindex.py b/pandas/tests/copy_view/index/test_datetimeindex.py index 6b289c856fd93..eb4dd7744bbdc 100644 --- a/pandas/tests/copy_view/index/test_datetimeindex.py +++ b/pandas/tests/copy_view/index/test_datetimeindex.py @@ -72,7 +72,7 @@ def test_constructor_copy_input_datetime_ea_default(): assert not tm.shares_memory(arr, idx.array) -def test_series_from_temporary_index_readonly_data(): +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 From f59ab9e39ff1b6d05a47a45ba8a606db45368e6c Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:33:31 -0600 Subject: [PATCH 04/30] TST: Add regression tests for GH#63388 (TimedeltaIndex copy behavior) --- .../copy_view/index/test_timedeltaindex.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandas/tests/copy_view/index/test_timedeltaindex.py b/pandas/tests/copy_view/index/test_timedeltaindex.py index 6984df86b00e3..d476acb6248b9 100644 --- a/pandas/tests/copy_view/index/test_timedeltaindex.py +++ b/pandas/tests/copy_view/index/test_timedeltaindex.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from pandas import ( @@ -7,6 +8,7 @@ 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" @@ -27,3 +29,31 @@ 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 = np.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) From 9221e5a0b7c21b29631bb99bfd3e19894208b043 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:41:05 -0600 Subject: [PATCH 05/30] ENH: Copy inputs in TimedeltaIndex constructor by default (GH#63388) --- pandas/core/indexes/timedeltas.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 2a3d5137242d0..15cf9b894e19e 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -7,6 +7,8 @@ cast, ) +import numpy as np + from pandas._libs import ( index as libindex, lib, @@ -23,9 +25,11 @@ is_scalar, pandas_dtype, ) +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.dtypes import ArrowDtype from pandas.core.dtypes.generic import ABCSeries +from pandas.core.arrays import ExtensionArray from pandas.core.arrays.timedeltas import TimedeltaArray import pandas.core.common as com from pandas.core.indexes.base import ( @@ -81,8 +85,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. @@ -158,11 +167,18 @@ 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) + if isinstance(data, (ExtensionArray, np.ndarray)): + # GH 63388 + if copy is not False: + if dtype is None or astype_is_view(data.dtype, dtype): + data = data.copy() + copy = False + if is_scalar(data): cls._raise_scalar_data_error(data) From 32ee4dcaa5c5a9a4891565c3d470bcbddcabb368 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:48:27 -0600 Subject: [PATCH 06/30] TST: correct test to use array instead of np.array --- pandas/tests/copy_view/index/test_datetimeindex.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/copy_view/index/test_datetimeindex.py b/pandas/tests/copy_view/index/test_datetimeindex.py index eb4dd7744bbdc..f223d6236e032 100644 --- a/pandas/tests/copy_view/index/test_datetimeindex.py +++ b/pandas/tests/copy_view/index/test_datetimeindex.py @@ -6,6 +6,7 @@ Series, Timestamp, date_range, + array ) import pandas._testing as tm from pandas.tests.copy_view.util import get_array @@ -67,7 +68,7 @@ def test_constructor_copy_input_datetime_ndarray_default(): def test_constructor_copy_input_datetime_ea_default(): # GH 63388 - arr = np.array(["2020-01-01", "2020-01-02"], dtype="datetime64[ns]") + arr = array(["2020-01-01", "2020-01-02"], dtype="datetime64[ns]") idx = DatetimeIndex(arr) assert not tm.shares_memory(arr, idx.array) From b55f4a04700f97a9e2bb5c17705f79e3101aacc5 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:50:05 -0600 Subject: [PATCH 07/30] TST: correct test to use array instead of np.array --- pandas/tests/copy_view/index/test_timedeltaindex.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/copy_view/index/test_timedeltaindex.py b/pandas/tests/copy_view/index/test_timedeltaindex.py index d476acb6248b9..0888665e469eb 100644 --- a/pandas/tests/copy_view/index/test_timedeltaindex.py +++ b/pandas/tests/copy_view/index/test_timedeltaindex.py @@ -6,6 +6,7 @@ Timedelta, TimedeltaIndex, timedelta_range, + array ) import pandas._testing as tm from pandas.tests.copy_view.util import get_array @@ -40,7 +41,7 @@ def test_constructor_copy_input_timedelta_ndarray_default(): def test_constructor_copy_input_timedelta_ea_default(): # GH 63388 - arr = np.array([1, 2], dtype="timedelta64[ns]") + arr = array([1, 2], dtype="timedelta64[ns]") idx = TimedeltaIndex(arr) assert not tm.shares_memory(arr, idx.array) From 91e78dae32323ee8f4cc82632980f2c2ce9b555a Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 07:56:07 -0600 Subject: [PATCH 08/30] TST: Add regression tests for GH#63388 (PeriodIndex copy behavior) --- .../tests/copy_view/index/test_periodindex.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pandas/tests/copy_view/index/test_periodindex.py b/pandas/tests/copy_view/index/test_periodindex.py index 2887b191038d2..29fce0fb6290b 100644 --- a/pandas/tests/copy_view/index/test_periodindex.py +++ b/pandas/tests/copy_view/index/test_periodindex.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from pandas import ( @@ -5,8 +6,10 @@ PeriodIndex, Series, period_range, + array ) 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" @@ -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) From 12527829880525cfdbcfb903c1e981460c699660 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:00:40 -0600 Subject: [PATCH 09/30] ENH: Copy inputs in PeriodIndex constructor by default (GH#63388) --- pandas/core/indexes/period.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index c3ad466a114a9..22118ae065aed 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -27,11 +27,13 @@ set_module, ) +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import is_integer 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 +from pandas.core.arrays import ExtensionArray from pandas.core.arrays.period import ( PeriodArray, period_array, @@ -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. @@ -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 @@ -231,6 +238,13 @@ def __new__( freq = validate_dtype_freq(dtype, freq) + if isinstance(data, (ExtensionArray, np.ndarray)): + # GH 63388 + if copy is not False: + if dtype is None or astype_is_view(data.dtype, dtype): + data = data.copy() + copy = False + # PeriodIndex allow PeriodIndex(period_index, freq=different) # Let's not encourage that kind of behavior in PeriodArray. From 923c949423ff95fd70c67aee511250f8eabfa92f Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:04:34 -0600 Subject: [PATCH 10/30] TST: Add regression tests for GH#63388 (IntervalIndex copy behavior) --- .../copy_view/index/test_intervalindex.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pandas/tests/copy_view/index/test_intervalindex.py diff --git a/pandas/tests/copy_view/index/test_intervalindex.py b/pandas/tests/copy_view/index/test_intervalindex.py new file mode 100644 index 0000000000000..630687dc0c102 --- /dev/null +++ b/pandas/tests/copy_view/index/test_intervalindex.py @@ -0,0 +1,32 @@ +import numpy as np + +from pandas import ( + IntervalIndex, + Series, + Interval, + 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) From 122f554a2e84ce54cc72ac7b7b8a63e379b4f54b Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:11:10 -0600 Subject: [PATCH 11/30] ENH: Copy inputs in IntervalIndex constructor by default (GH#63388) --- pandas/core/indexes/interval.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 7bb64503a469e..e6b7637f431fb 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -35,6 +35,7 @@ ) from pandas.util._exceptions import rewrite_exception +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.cast import ( find_common_type, infer_dtype_from_scalar, @@ -61,6 +62,7 @@ from pandas.core.dtypes.missing import is_valid_na_for_dtype from pandas.core.algorithms import unique +from pandas.core.arrays import ExtensionArray from pandas.core.arrays.datetimelike import validate_periods from pandas.core.arrays.interval import ( IntervalArray, @@ -169,8 +171,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 @@ -252,12 +259,19 @@ 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) + if isinstance(data, (ExtensionArray, np.ndarray)): + # GH#63388 + if copy is not False: + if dtype is None or astype_is_view(data.dtype, dtype): + data = data.copy() + copy = False + with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray( data, From 66a37fe1ea3b841f694d1d8f724621954076fdd9 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:17:29 -0600 Subject: [PATCH 12/30] STYLE: Apply isort fixes --- pandas/core/indexes/datetimes.py | 4 ++-- pandas/core/indexes/timedeltas.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 21944a0e62c79..4bd8452e3657c 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -37,20 +37,20 @@ ) from pandas.util._exceptions import find_stack_level +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import is_scalar from pandas.core.dtypes.dtypes import ( ArrowDtype, DatetimeTZDtype, ) -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.generic import ABCSeries from pandas.core.dtypes.missing import is_valid_na_for_dtype +from pandas.core.arrays import ExtensionArray from pandas.core.arrays.datetimes import ( DatetimeArray, tz_to_dtype, ) -from pandas.core.arrays import ExtensionArray import pandas.core.common as com from pandas.core.indexes.base import ( Index, diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 15cf9b894e19e..811d637c73166 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -21,11 +21,11 @@ from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit from pandas.util._decorators import set_module +from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import ( is_scalar, pandas_dtype, ) -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.dtypes import ArrowDtype from pandas.core.dtypes.generic import ABCSeries From 5a211cc0767e6b9858810b53cfab7491fcbbb026 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:23:37 -0600 Subject: [PATCH 13/30] DOC: Add release note for GH#63388 --- doc/source/whatsnew/v3.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index a1de10f61306a..ce4e2d26e6270 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -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`) .. --------------------------------------------------------------------------- .. _whatsnew_300.deprecations: From 937333304c890818660318afdf5e1e1592e44874 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:55:49 -0600 Subject: [PATCH 14/30] TYP: Fix mypy errors with type ignores --- pandas/core/indexes/datetimes.py | 6 ++++-- pandas/core/indexes/interval.py | 6 ++++-- pandas/core/indexes/period.py | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 4bd8452e3657c..1a395803a7f0a 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -689,7 +689,9 @@ def __new__( if isinstance(data, (ExtensionArray, np.ndarray)): # GH 63388 if copy is not False: - if dtype is None or astype_is_view(data.dtype, dtype): + if dtype is None or astype_is_view( + data.dtype, dtype # type: ignore[arg-type] + ): data = data.copy() copy = False @@ -708,7 +710,7 @@ def __new__( dtarr = DatetimeArray._from_sequence_not_strict( data, dtype=dtype, - copy=copy, + copy=bool(copy), tz=tz, freq=freq, dayfirst=dayfirst, diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index e6b7637f431fb..a2a74c4a7f4a7 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -268,7 +268,9 @@ def __new__( if isinstance(data, (ExtensionArray, np.ndarray)): # GH#63388 if copy is not False: - if dtype is None or astype_is_view(data.dtype, dtype): + if dtype is None or astype_is_view( + data.dtype, dtype # type: ignore[arg-type] + ): data = data.copy() copy = False @@ -276,7 +278,7 @@ def __new__( array = IntervalArray( data, closed=closed, - copy=copy, + copy=bool(copy), dtype=dtype, verify_integrity=verify_integrity, ) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 22118ae065aed..0b4e27f70d7c1 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -241,7 +241,9 @@ def __new__( if isinstance(data, (ExtensionArray, np.ndarray)): # GH 63388 if copy is not False: - if dtype is None or astype_is_view(data.dtype, dtype): + if dtype is None or astype_is_view( + data.dtype, dtype # type: ignore[arg-type] + ): data = data.copy() copy = False From bc3c54af06a3ce033376d980ab3c72c0b6b3107f Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 08:58:27 -0600 Subject: [PATCH 15/30] TYP: Fix mypy errors using bool(copy) and ignores --- pandas/core/indexes/timedeltas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 811d637c73166..a92347feb407f 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -208,7 +208,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)): From 6731252b9bdcf331310c28e179cc31b047f15a52 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 09:14:19 -0600 Subject: [PATCH 16/30] STY: Apply pre-commit fixes --- pandas/core/indexes/datetimes.py | 5 +++-- pandas/core/indexes/interval.py | 3 ++- pandas/core/indexes/period.py | 3 ++- pandas/tests/copy_view/index/test_datetimeindex.py | 5 ++--- pandas/tests/copy_view/index/test_intervalindex.py | 7 ++----- pandas/tests/copy_view/index/test_periodindex.py | 12 ++++++------ pandas/tests/copy_view/index/test_timedeltaindex.py | 7 +++---- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 1a395803a7f0a..6fb8331aad9b4 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -690,8 +690,9 @@ def __new__( # GH 63388 if copy is not False: if dtype is None or astype_is_view( - data.dtype, dtype # type: ignore[arg-type] - ): + data.dtype, + dtype, # type: ignore[arg-type] + ): data = data.copy() copy = False diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index a2a74c4a7f4a7..4d403e5a6704c 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -269,7 +269,8 @@ def __new__( # GH#63388 if copy is not False: if dtype is None or astype_is_view( - data.dtype, dtype # type: ignore[arg-type] + data.dtype, + dtype, # type: ignore[arg-type] ): data = data.copy() copy = False diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 0b4e27f70d7c1..b09dab44284aa 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -242,7 +242,8 @@ def __new__( # GH 63388 if copy is not False: if dtype is None or astype_is_view( - data.dtype, dtype # type: ignore[arg-type] + data.dtype, + dtype, # type: ignore[arg-type] ): data = data.copy() copy = False diff --git a/pandas/tests/copy_view/index/test_datetimeindex.py b/pandas/tests/copy_view/index/test_datetimeindex.py index f223d6236e032..ddbff4bafdeff 100644 --- a/pandas/tests/copy_view/index/test_datetimeindex.py +++ b/pandas/tests/copy_view/index/test_datetimeindex.py @@ -5,8 +5,8 @@ DatetimeIndex, Series, Timestamp, + array, date_range, - array ) import pandas._testing as tm from pandas.tests.copy_view.util import get_array @@ -81,7 +81,6 @@ def test_series_from_temporary_datetimeindex_readonly_data(): 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]" + [Timestamp("2020-01-01"), Timestamp("2020-01-02")], dtype="datetime64[ns]" ) tm.assert_series_equal(ser, expected) diff --git a/pandas/tests/copy_view/index/test_intervalindex.py b/pandas/tests/copy_view/index/test_intervalindex.py index 630687dc0c102..d30415d05e4e4 100644 --- a/pandas/tests/copy_view/index/test_intervalindex.py +++ b/pandas/tests/copy_view/index/test_intervalindex.py @@ -1,9 +1,9 @@ import numpy as np from pandas import ( + Interval, IntervalIndex, Series, - Interval, array, ) import pandas._testing as tm @@ -25,8 +25,5 @@ def test_series_from_temporary_intervalindex_readonly_data(): 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]' - ) + expected = Series([Interval(5, 6), Interval(1, 2)], dtype="interval[int64, right]") tm.assert_series_equal(ser, expected) diff --git a/pandas/tests/copy_view/index/test_periodindex.py b/pandas/tests/copy_view/index/test_periodindex.py index 29fce0fb6290b..5f741d123f4a8 100644 --- a/pandas/tests/copy_view/index/test_periodindex.py +++ b/pandas/tests/copy_view/index/test_periodindex.py @@ -5,8 +5,8 @@ Period, PeriodIndex, Series, + array, period_range, - array ) import pandas._testing as tm from pandas.tests.copy_view.util import get_array @@ -28,20 +28,20 @@ def test_periodindex(box): def test_constructor_copy_input_period_ea_default(): # GH 63388 - arr = array(['2020-01-01', '2020-01-02'], dtype='period[D]') + 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 = 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') + 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]' + [Period("2022-01-01", freq="D"), Period("2020-01-02", freq="D")], + dtype="period[D]", ) tm.assert_series_equal(ser, expected) diff --git a/pandas/tests/copy_view/index/test_timedeltaindex.py b/pandas/tests/copy_view/index/test_timedeltaindex.py index 0888665e469eb..9f4dcc39fe6f6 100644 --- a/pandas/tests/copy_view/index/test_timedeltaindex.py +++ b/pandas/tests/copy_view/index/test_timedeltaindex.py @@ -5,8 +5,8 @@ Series, Timedelta, TimedeltaIndex, + array, timedelta_range, - array ) import pandas._testing as tm from pandas.tests.copy_view.util import get_array @@ -49,12 +49,11 @@ def test_constructor_copy_input_timedelta_ea_default(): def test_series_from_temporary_timedeltaindex_readonly_data(): # GH 63388 arr = np.array([1, 2], dtype="timedelta64[ns]") - arr.flags.writeable = False + 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]" + [Timedelta(days=1), Timedelta(nanoseconds=2)], dtype="timedelta64[ns]" ) tm.assert_series_equal(ser, expected) From 9acc8d1d6f11916c1069273a433e8f984655a771 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 09:48:46 -0600 Subject: [PATCH 17/30] FIX: Update regression test for copy default --- pandas/tests/indexes/datetimes/test_constructors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 4aa4c81558fa9..06ef4c057b5cb 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -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() From 180fa7ed1e16b307f4a907b57943ac0d686289a6 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 09:50:17 -0600 Subject: [PATCH 18/30] FIX: handle string dtypes --- pandas/core/indexes/datetimes.py | 7 +++++-- pandas/core/indexes/interval.py | 2 +- pandas/core/indexes/period.py | 7 +++++-- pandas/core/indexes/timedeltas.py | 5 ++++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 6fb8331aad9b4..bb5d47efbbc36 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -38,7 +38,10 @@ from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.astype import astype_is_view -from pandas.core.dtypes.common import is_scalar +from pandas.core.dtypes.common import ( + is_scalar, + pandas_dtype, +) from pandas.core.dtypes.dtypes import ( ArrowDtype, DatetimeTZDtype, @@ -691,7 +694,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - dtype, # type: ignore[arg-type] + pandas_dtype(dtype), # type: ignore[arg-type] ): data = data.copy() copy = False diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 4d403e5a6704c..17aabd1f495ca 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -270,7 +270,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - dtype, # type: ignore[arg-type] + pandas_dtype(dtype), # type: ignore[arg-type] ): data = data.copy() copy = False diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b09dab44284aa..7308d82169cce 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -28,7 +28,10 @@ ) from pandas.core.dtypes.astype import astype_is_view -from pandas.core.dtypes.common import is_integer +from pandas.core.dtypes.common import ( + is_integer, + pandas_dtype, +) 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 @@ -243,7 +246,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - dtype, # type: ignore[arg-type] + pandas_dtype(dtype), # type: ignore[arg-type] ): data = data.copy() copy = False diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index a92347feb407f..5a09cbd58cfc4 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -175,7 +175,10 @@ def __new__( if isinstance(data, (ExtensionArray, np.ndarray)): # GH 63388 if copy is not False: - if dtype is None or astype_is_view(data.dtype, dtype): + if dtype is None or astype_is_view( + data.dtype, + pandas_dtype(dtype), + ): data = data.copy() copy = False From f7a00a46ca969eefc62cc2f5b2c047de2f024ce9 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 09:54:59 -0600 Subject: [PATCH 19/30] FIX: Update test_array_tz to use copy=False --- pandas/tests/arrays/test_datetimelike.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/arrays/test_datetimelike.py b/pandas/tests/arrays/test_datetimelike.py index 60a80f9af78c5..b4c6060b11403 100644 --- a/pandas/tests/arrays/test_datetimelike.py +++ b/pandas/tests/arrays/test_datetimelike.py @@ -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]") From ca90c29343f37710669471d775e84f9f0d2bab2c Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 09:57:55 -0600 Subject: [PATCH 20/30] STY: apply pre-commit fixes --- pandas/core/indexes/timedeltas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 5a09cbd58cfc4..b5b9fd6b6d046 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -176,7 +176,7 @@ def __new__( # GH 63388 if copy is not False: if dtype is None or astype_is_view( - data.dtype, + data.dtype, pandas_dtype(dtype), ): data = data.copy() From 75a3aac7aaa2a8e6bbcd05358a4183b603fcb345 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 10:03:01 -0600 Subject: [PATCH 21/30] CLN: Remove unused type ignores --- pandas/core/indexes/datetimes.py | 2 +- pandas/core/indexes/interval.py | 2 +- pandas/core/indexes/period.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index bb5d47efbbc36..f54547a9adba4 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -694,7 +694,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - pandas_dtype(dtype), # type: ignore[arg-type] + pandas_dtype(dtype), ): data = data.copy() copy = False diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 17aabd1f495ca..82656826204e6 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -270,7 +270,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - pandas_dtype(dtype), # type: ignore[arg-type] + pandas_dtype(dtype), ): data = data.copy() copy = False diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 7308d82169cce..88aed71e03f50 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -246,7 +246,7 @@ def __new__( if copy is not False: if dtype is None or astype_is_view( data.dtype, - pandas_dtype(dtype), # type: ignore[arg-type] + pandas_dtype(dtype), ): data = data.copy() copy = False From e689f7bbb1deee030fb6f85366fa0b2e032b1bee Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 11:58:45 -0600 Subject: [PATCH 22/30] REF: Add Index._maybe_copy_input helper (GH#63388) Extracts the copy/view validation logic into a shared classmethod on the base Index class, as requested during review. --- pandas/core/indexes/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 3239621468c5a..44df26f738e21 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5196,6 +5196,19 @@ def _raise_scalar_data_error(cls, data): "was passed" ) + @classmethod + def _maybe_copy_input(cls, data, copy, dtype): + """ + 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 + def _validate_fill_value(self, value): """ Check if the value can be inserted into our array without casting, From 65d80120823414be6d97e6af1af491cf7b3a3172 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Wed, 17 Dec 2025 11:59:14 -0600 Subject: [PATCH 23/30] REF: Use _maybe_copy_input in Index subclasses (GH#63388) Updates DatetimeIndex, TimedeltaIndex, PeriodIndex, and IntervalIndex to use the shared validation logic, reducing code duplication. --- pandas/core/indexes/datetimes.py | 14 ++------------ pandas/core/indexes/interval.py | 13 ++----------- pandas/core/indexes/period.py | 14 ++------------ pandas/core/indexes/timedeltas.py | 15 ++------------- 4 files changed, 8 insertions(+), 48 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index f54547a9adba4..bebdcc4e84c1a 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -37,10 +37,8 @@ ) from pandas.util._exceptions import find_stack_level -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import ( is_scalar, - pandas_dtype, ) from pandas.core.dtypes.dtypes import ( ArrowDtype, @@ -49,7 +47,6 @@ from pandas.core.dtypes.generic import ABCSeries from pandas.core.dtypes.missing import is_valid_na_for_dtype -from pandas.core.arrays import ExtensionArray from pandas.core.arrays.datetimes import ( DatetimeArray, tz_to_dtype, @@ -689,15 +686,8 @@ def __new__( name = maybe_extract_name(name, data, cls) - if isinstance(data, (ExtensionArray, np.ndarray)): - # GH 63388 - if copy is not False: - if dtype is None or astype_is_view( - data.dtype, - pandas_dtype(dtype), - ): - data = data.copy() - copy = False + # GH#63388 + data, copy = cls._maybe_copy_input(data, copy, dtype) if ( isinstance(data, DatetimeArray) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 82656826204e6..032a116f0b4b5 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -35,7 +35,6 @@ ) from pandas.util._exceptions import rewrite_exception -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.cast import ( find_common_type, infer_dtype_from_scalar, @@ -62,7 +61,6 @@ from pandas.core.dtypes.missing import is_valid_na_for_dtype from pandas.core.algorithms import unique -from pandas.core.arrays import ExtensionArray from pandas.core.arrays.datetimelike import validate_periods from pandas.core.arrays.interval import ( IntervalArray, @@ -265,15 +263,8 @@ def __new__( ) -> Self: name = maybe_extract_name(name, data, cls) - if isinstance(data, (ExtensionArray, np.ndarray)): - # GH#63388 - if copy is not False: - if dtype is None or astype_is_view( - data.dtype, - pandas_dtype(dtype), - ): - data = data.copy() - copy = False + # GH#63388 + data, copy = cls._maybe_copy_input(data, copy, dtype) with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray( diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 88aed71e03f50..afe88d857733b 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -27,16 +27,13 @@ set_module, ) -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import ( is_integer, - pandas_dtype, ) 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 -from pandas.core.arrays import ExtensionArray from pandas.core.arrays.period import ( PeriodArray, period_array, @@ -241,15 +238,8 @@ def __new__( freq = validate_dtype_freq(dtype, freq) - if isinstance(data, (ExtensionArray, np.ndarray)): - # GH 63388 - if copy is not False: - if dtype is None or astype_is_view( - data.dtype, - pandas_dtype(dtype), - ): - data = data.copy() - copy = False + # 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. diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index b5b9fd6b6d046..a9e2e0eeb8a94 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -7,8 +7,6 @@ cast, ) -import numpy as np - from pandas._libs import ( index as libindex, lib, @@ -21,7 +19,6 @@ from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit from pandas.util._decorators import set_module -from pandas.core.dtypes.astype import astype_is_view from pandas.core.dtypes.common import ( is_scalar, pandas_dtype, @@ -29,7 +26,6 @@ from pandas.core.dtypes.dtypes import ArrowDtype from pandas.core.dtypes.generic import ABCSeries -from pandas.core.arrays import ExtensionArray from pandas.core.arrays.timedeltas import TimedeltaArray import pandas.core.common as com from pandas.core.indexes.base import ( @@ -172,15 +168,8 @@ def __new__( ): name = maybe_extract_name(name, data, cls) - if isinstance(data, (ExtensionArray, np.ndarray)): - # GH 63388 - if copy is not False: - if dtype is None or astype_is_view( - data.dtype, - pandas_dtype(dtype), - ): - data = data.copy() - copy = False + # GH#63388 + data, copy = cls._maybe_copy_input(data, copy, dtype) if is_scalar(data): cls._raise_scalar_data_error(data) From 05d49242787d97489d73385665e61428f8f24a99 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 05:37:32 -0600 Subject: [PATCH 24/30] DOC: Update release note for GH#63388 Clarifies that only array/EA inputs are copied by default, as requested by reviewers. --- doc/source/whatsnew/v3.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index ce4e2d26e6270..b02ca87b0ff78 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -820,7 +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`) +- All Index constructors now copy ``numpy.ndarray`` and ``ExtensionArray`` inputs by default when ``copy=None``, consistent with :class:`Series` behavior (:issue:`63388`) .. --------------------------------------------------------------------------- .. _whatsnew_300.deprecations: From 1e61eb68561af421416ae5552ac21a6abdcb1645 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 05:42:48 -0600 Subject: [PATCH 25/30] REF: Rename copy helper to _maybe_copy_array_input and add type hints (GH#63388) --- pandas/core/indexes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 44df26f738e21..5e50e1a535df9 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5197,7 +5197,7 @@ def _raise_scalar_data_error(cls, data): ) @classmethod - 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 From 5e87eabaa5e119fb523315c1006d20ea5063e875 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 05:47:02 -0600 Subject: [PATCH 26/30] REF: update to use renamed _maybe_copy_array_input classmethod (GH#63388) --- pandas/core/indexes/datetimes.py | 2 +- pandas/core/indexes/interval.py | 2 +- pandas/core/indexes/period.py | 2 +- pandas/core/indexes/timedeltas.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index bebdcc4e84c1a..2edb714e786ab 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -687,7 +687,7 @@ def __new__( name = maybe_extract_name(name, data, cls) # GH#63388 - data, copy = cls._maybe_copy_input(data, copy, dtype) + data, copy = cls._maybe_copy_array_input(data, copy, dtype) if ( isinstance(data, DatetimeArray) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 032a116f0b4b5..3bf1357cfa68f 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -264,7 +264,7 @@ def __new__( name = maybe_extract_name(name, data, cls) # GH#63388 - data, copy = cls._maybe_copy_input(data, copy, dtype) + data, copy = cls._maybe_copy_array_input(data, copy, dtype) with rewrite_exception("IntervalArray", cls.__name__): array = IntervalArray( diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index afe88d857733b..51869505d0dab 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -239,7 +239,7 @@ def __new__( freq = validate_dtype_freq(dtype, freq) # GH#63388 - data, copy = cls._maybe_copy_input(data, copy, dtype) + data, copy = cls._maybe_copy_array_input(data, copy, dtype) # PeriodIndex allow PeriodIndex(period_index, freq=different) # Let's not encourage that kind of behavior in PeriodArray. diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index a9e2e0eeb8a94..25910a7b77de2 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -169,7 +169,7 @@ def __new__( name = maybe_extract_name(name, data, cls) # GH#63388 - data, copy = cls._maybe_copy_input(data, copy, dtype) + data, copy = cls._maybe_copy_array_input(data, copy, dtype) if is_scalar(data): cls._raise_scalar_data_error(data) From c290a305161bdd2e2a544373e1799964dd4e6c71 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 05:53:29 -0600 Subject: [PATCH 27/30] REF: Return strict bool from copy helper and clean up call sites (GH#63388) Updates _maybe_copy_array_input to return 'bool(copy)' ensuring strictly boolean output. Removes now-redundant bool() casts in subclass constructors. --- pandas/core/indexes/base.py | 2 +- pandas/core/indexes/datetimes.py | 2 +- pandas/core/indexes/interval.py | 2 +- pandas/core/indexes/timedeltas.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 5e50e1a535df9..9a1b57a8a2b6a 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5207,7 +5207,7 @@ def _maybe_copy_array_input(cls, data, copy: bool | None, dtype) -> tuple[Any, b if dtype is None or astype_is_view(data.dtype, pandas_dtype(dtype)): data = data.copy() copy = False - return data, copy + return data, bool(copy) def _validate_fill_value(self, value): """ diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 2edb714e786ab..a702549900213 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -704,7 +704,7 @@ def __new__( dtarr = DatetimeArray._from_sequence_not_strict( data, dtype=dtype, - copy=bool(copy), + copy=copy, tz=tz, freq=freq, dayfirst=dayfirst, diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 3bf1357cfa68f..1def317bc1a88 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -270,7 +270,7 @@ def __new__( array = IntervalArray( data, closed=closed, - copy=bool(copy), + copy=copy, dtype=dtype, verify_integrity=verify_integrity, ) diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 25910a7b77de2..725ef8cae7120 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -200,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=bool(copy) + data, freq=freq, unit=None, dtype=dtype, copy=copy ) refs = None if not copy and isinstance(data, (ABCSeries, Index)): From 02af4fb6b338385bebaa7783575d469ecfe25aa1 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 05:59:04 -0600 Subject: [PATCH 28/30] STY: Revert import formatting (GH#63388) --- pandas/core/indexes/datetimes.py | 4 +--- pandas/core/indexes/period.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index a702549900213..72b009a344193 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -37,9 +37,7 @@ ) 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, diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 51869505d0dab..b8a25ab0da693 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -27,9 +27,7 @@ set_module, ) -from pandas.core.dtypes.common import ( - is_integer, -) +from pandas.core.dtypes.common import is_integer 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 From e2b0f49dfcdab1e82f3094ea8036a656f4b2042e Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 06:18:48 -0600 Subject: [PATCH 29/30] REF: Use copy helper for GH#63306 logic in Index constructor --- pandas/core/indexes/base.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 6fd3eb22aa78f..c7fc8633d03c3 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -505,12 +505,8 @@ def __new__( if not copy and isinstance(data, (ABCSeries, Index)): refs = data._references - if isinstance(data, (ExtensionArray, np.ndarray)): - # GH 63306 - if copy is not False: - if dtype is None or astype_is_view(data.dtype, dtype): - data = data.copy() - copy = False + # GH 63306, GH 63388 + data, copy = cls._maybe_copy_array_input(data, copy, dtype) # range if isinstance(data, (range, RangeIndex)): From f8721aa858d9da729425f5900f717c58e275b353 Mon Sep 17 00:00:00 2001 From: zacharym-collins Date: Thu, 18 Dec 2025 06:22:36 -0600 Subject: [PATCH 30/30] STY: Apply ruff formatting to method signature --- pandas/core/indexes/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index c7fc8633d03c3..ecfd26a412c2d 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -5194,7 +5194,9 @@ def _raise_scalar_data_error(cls, data): ) @classmethod - def _maybe_copy_array_input(cls, data, copy: bool | None, dtype) -> tuple[Any, bool]: + 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