Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 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
05d4924
DOC: Update release note for GH#63388
zacharym-collins Dec 18, 2025
1e61eb6
REF: Rename copy helper to _maybe_copy_array_input and add type hints…
zacharym-collins Dec 18, 2025
5e87eab
REF: update to use renamed _maybe_copy_array_input classmethod (GH#63…
zacharym-collins Dec 18, 2025
c290a30
REF: Return strict bool from copy helper and clean up call sites (GH#…
zacharym-collins Dec 18, 2025
02af4fb
STY: Revert import formatting (GH#63388)
zacharym-collins Dec 18, 2025
fd6de1d
Merge remote-tracking branch 'upstream/main' into api-copy-index-subc…
zacharym-collins Dec 18, 2025
e2b0f49
REF: Use copy helper for GH#63306 logic in Index constructor
zacharym-collins Dec 18, 2025
f8721aa
STY: Apply ruff formatting to method signature
zacharym-collins Dec 18, 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
30 changes: 25 additions & 5 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@
)
from pandas.util._exceptions import find_stack_level

from pandas.core.dtypes.common import is_scalar
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,
DatetimeTZDtype,
)
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,
Expand Down Expand Up @@ -181,8 +186,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 +679,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 +689,16 @@ 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

if (
isinstance(data, DatetimeArray)
and freq is lib.no_default
Expand All @@ -694,7 +714,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
25 changes: 21 additions & 4 deletions pandas/core/indexes/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -252,17 +259,27 @@ 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,
pandas_dtype(dtype),
):
data = data.copy()
copy = False

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

from pandas.core.dtypes.common import is_integer
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,
Expand Down Expand Up @@ -101,8 +106,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 +230,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 +241,16 @@ 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

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

Expand Down
27 changes: 23 additions & 4 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
cast,
)

import numpy as np

from pandas._libs import (
index as libindex,
lib,
Expand All @@ -19,13 +21,15 @@
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.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 (
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -158,11 +167,21 @@ 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,
pandas_dtype(dtype),
):
data = data.copy()
copy = False

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

Expand Down Expand Up @@ -192,7 +211,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)
Loading
Loading