Skip to content

Commit bb7dd2c

Browse files
authored
DEPR: DatetimeIndex indexing with mismatched tzawareness (#49492)
* DEPR: DatetimeIndex indexing with mismatched tzawareness * clarify whatsnew
1 parent ca99c94 commit bb7dd2c

File tree

7 files changed

+75
-79
lines changed

7 files changed

+75
-79
lines changed

doc/source/whatsnew/v2.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ Removal of prior version deprecations/changes
427427
- Changed behavior of empty data passed into :class:`Series`; the default dtype will be ``object`` instead of ``float64`` (:issue:`29405`)
428428
- Changed the behavior of :meth:`DatetimeIndex.union`, :meth:`DatetimeIndex.intersection`, and :meth:`DatetimeIndex.symmetric_difference` with mismatched timezones to convert to UTC instead of casting to object dtype (:issue:`39328`)
429429
- Changed the behavior of :func:`to_datetime` with argument "now" with ``utc=False`` to match ``Timestamp("now")`` (:issue:`18705`)
430+
- Changed the behavior of indexing on a timezone-aware :class:`DatetimeIndex` with a timezone-naive ``datetime`` object or vice-versa; these now behave like any other non-comparable type by raising ``KeyError`` (:issue:`36148`)
430431
- Changed the behavior of :meth:`Index.reindex`, :meth:`Series.reindex`, and :meth:`DataFrame.reindex` with a ``datetime64`` dtype and a ``datetime.date`` object for ``fill_value``; these are no longer considered equivalent to ``datetime.datetime`` objects so the reindex casts to object dtype (:issue:`39767`)
431432
- Changed behavior of :meth:`SparseArray.astype` when given a dtype that is not explicitly ``SparseDtype``, cast to the exact requested dtype rather than silently using a ``SparseDtype`` instead (:issue:`34457`)
432433
- Changed behavior of :meth:`Index.ravel` to return a view on the original :class:`Index` instead of a ``np.ndarray`` (:issue:`36900`)

pandas/core/indexes/datetimes.py

+17-24
Original file line numberDiff line numberDiff line change
@@ -552,31 +552,24 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
552552
end = self._maybe_cast_for_get_loc(end)
553553
return start, end
554554

555+
def _disallow_mismatched_indexing(self, key, one_way: bool = False) -> None:
556+
"""
557+
Check for mismatched-tzawareness indexing and re-raise as KeyError.
558+
"""
559+
try:
560+
self._deprecate_mismatched_indexing(key, one_way=one_way)
561+
except TypeError as err:
562+
raise KeyError(key) from err
563+
555564
def _deprecate_mismatched_indexing(self, key, one_way: bool = False) -> None:
556565
# GH#36148
557566
# we get here with isinstance(key, self._data._recognized_scalars)
558-
try:
559-
self._data._assert_tzawareness_compat(key)
560-
except TypeError:
561-
if self.tz is None:
562-
msg = (
563-
"Indexing a timezone-naive DatetimeIndex with a "
564-
"timezone-aware datetime is deprecated and will "
565-
"raise KeyError in a future version. "
566-
"Use a timezone-naive object instead."
567-
)
568-
elif one_way:
569-
# we special-case timezone-naive strings and timezone-aware
570-
# DatetimeIndex
571-
return
572-
else:
573-
msg = (
574-
"Indexing a timezone-aware DatetimeIndex with a "
575-
"timezone-naive datetime is deprecated and will "
576-
"raise KeyError in a future version. "
577-
"Use a timezone-aware object instead."
578-
)
579-
warnings.warn(msg, FutureWarning, stacklevel=find_stack_level())
567+
if self.tz is not None and one_way:
568+
# we special-case timezone-naive strings and timezone-aware
569+
# DatetimeIndex
570+
return
571+
572+
self._data._assert_tzawareness_compat(key)
580573

581574
def get_loc(self, key, method=None, tolerance=None):
582575
"""
@@ -594,7 +587,7 @@ def get_loc(self, key, method=None, tolerance=None):
594587

595588
if isinstance(key, self._data._recognized_scalars):
596589
# needed to localize naive datetimes
597-
self._deprecate_mismatched_indexing(key)
590+
self._disallow_mismatched_indexing(key)
598591
key = self._maybe_cast_for_get_loc(key)
599592

600593
elif isinstance(key, str):
@@ -603,7 +596,7 @@ def get_loc(self, key, method=None, tolerance=None):
603596
parsed, reso = self._parse_with_reso(key)
604597
except ValueError as err:
605598
raise KeyError(key) from err
606-
self._deprecate_mismatched_indexing(parsed, one_way=True)
599+
self._disallow_mismatched_indexing(parsed, one_way=True)
607600

608601
if self._can_partial_date_slice(reso):
609602
try:

pandas/tests/frame/methods/test_reset_index.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -350,20 +350,14 @@ def test_reset_index_multiindex_nan(self):
350350
)
351351
def test_reset_index_with_datetimeindex_cols(self, name):
352352
# GH#5818
353-
warn = None
354-
if isinstance(name, Timestamp) and name.tz is not None:
355-
# _deprecate_mismatched_indexing
356-
warn = FutureWarning
357-
358353
df = DataFrame(
359354
[[1, 2], [3, 4]],
360355
columns=date_range("1/1/2013", "1/2/2013"),
361356
index=["A", "B"],
362357
)
363358
df.index.name = name
364359

365-
with tm.assert_produces_warning(warn):
366-
result = df.reset_index()
360+
result = df.reset_index()
367361

368362
item = name if name is not None else "index"
369363
columns = Index([item, datetime(2013, 1, 1), datetime(2013, 1, 2)])

pandas/tests/indexes/datetimes/test_indexing.py

+19-13
Original file line numberDiff line numberDiff line change
@@ -718,11 +718,13 @@ def test_get_slice_bounds_datetime_within(
718718
index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz)
719719
key = box(year=2000, month=1, day=7)
720720

721-
warn = None if tz is None else FutureWarning
722-
with tm.assert_produces_warning(warn):
723-
# GH#36148 will require tzawareness-compat
721+
if tz is not None:
722+
with pytest.raises(TypeError, match="Cannot compare tz-naive"):
723+
# GH#36148 we require tzawareness-compat as of 2.0
724+
index.get_slice_bound(key, side=side)
725+
else:
724726
result = index.get_slice_bound(key, side=side)
725-
assert result == expected
727+
assert result == expected
726728

727729
@pytest.mark.parametrize("box", [datetime, Timestamp])
728730
@pytest.mark.parametrize("side", ["left", "right"])
@@ -735,11 +737,13 @@ def test_get_slice_bounds_datetime_outside(
735737
index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz)
736738
key = box(year=year, month=1, day=7)
737739

738-
warn = None if tz is None else FutureWarning
739-
with tm.assert_produces_warning(warn):
740-
# GH#36148 will require tzawareness-compat
740+
if tz is not None:
741+
with pytest.raises(TypeError, match="Cannot compare tz-naive"):
742+
# GH#36148 we require tzawareness-compat as of 2.0
743+
index.get_slice_bound(key, side=side)
744+
else:
741745
result = index.get_slice_bound(key, side=side)
742-
assert result == expected
746+
assert result == expected
743747

744748
@pytest.mark.parametrize("box", [datetime, Timestamp])
745749
def test_slice_datetime_locs(self, box, tz_aware_fixture):
@@ -748,12 +752,14 @@ def test_slice_datetime_locs(self, box, tz_aware_fixture):
748752
index = DatetimeIndex(["2010-01-01", "2010-01-03"]).tz_localize(tz)
749753
key = box(2010, 1, 1)
750754

751-
warn = None if tz is None else FutureWarning
752-
with tm.assert_produces_warning(warn):
753-
# GH#36148 will require tzawareness-compat
755+
if tz is not None:
756+
with pytest.raises(TypeError, match="Cannot compare tz-naive"):
757+
# GH#36148 we require tzawareness-compat as of 2.0
758+
index.slice_locs(key, box(2010, 1, 2))
759+
else:
754760
result = index.slice_locs(key, box(2010, 1, 2))
755-
expected = (0, 1)
756-
assert result == expected
761+
expected = (0, 1)
762+
assert result == expected
757763

758764

759765
class TestIndexerBetweenTime:

pandas/tests/indexing/test_datetime.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import re
2+
3+
import pytest
4+
15
import pandas as pd
26
from pandas import (
37
DataFrame,
@@ -17,14 +21,12 @@ def test_get_loc_naive_dti_aware_str_deprecated(self):
1721
ser = Series(range(100), index=dti)
1822

1923
key = "2013-01-01 00:00:00.000000050+0000"
20-
msg = "Indexing a timezone-naive DatetimeIndex with a timezone-aware datetime"
21-
with tm.assert_produces_warning(FutureWarning, match=msg):
22-
res = ser[key]
23-
assert res == 0
24-
25-
with tm.assert_produces_warning(FutureWarning, match=msg):
26-
loc = dti.get_loc(key)
27-
assert loc == 0
24+
msg = re.escape(repr(key))
25+
with pytest.raises(KeyError, match=msg):
26+
ser[key]
27+
28+
with pytest.raises(KeyError, match=msg):
29+
dti.get_loc(key)
2830

2931
def test_indexing_with_datetime_tz(self):
3032

pandas/tests/series/indexing/test_datetime.py

+23-25
Original file line numberDiff line numberDiff line change
@@ -140,45 +140,43 @@ def test_getitem_setitem_datetimeindex():
140140
msg = "Cannot compare tz-naive and tz-aware datetime-like objects"
141141
naive = datetime(1990, 1, 1, 4)
142142
for key in [naive, Timestamp(naive), np.datetime64(naive, "ns")]:
143-
with tm.assert_produces_warning(FutureWarning):
144-
# GH#36148 will require tzawareness compat
145-
result = ts[key]
146-
expected = ts[4]
147-
assert result == expected
143+
with pytest.raises(KeyError, match=re.escape(repr(key))):
144+
# GH#36148 as of 2.0 we require tzawareness-compat
145+
ts[key]
148146

149147
result = ts.copy()
150-
with tm.assert_produces_warning(FutureWarning):
151-
# GH#36148 will require tzawareness compat
152-
result[datetime(1990, 1, 1, 4)] = 0
153-
with tm.assert_produces_warning(FutureWarning):
154-
# GH#36148 will require tzawareness compat
155-
result[datetime(1990, 1, 1, 4)] = ts[4]
156-
tm.assert_series_equal(result, ts)
148+
# GH#36148 as of 2.0 we do not ignore tzawareness mismatch in indexing,
149+
# so setting it as a new key casts to object rather than matching
150+
# rng[4]
151+
result[naive] = ts[4]
152+
assert result.index.dtype == object
153+
tm.assert_index_equal(result.index[:-1], rng.astype(object))
154+
assert result.index[-1] == naive
157155

158-
with tm.assert_produces_warning(FutureWarning):
159-
# GH#36148 will require tzawareness compat
160-
result = ts[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)]
161-
expected = ts[4:8]
162-
tm.assert_series_equal(result, expected)
156+
msg = "Cannot compare tz-naive and tz-aware datetime-like objects"
157+
with pytest.raises(TypeError, match=msg):
158+
# GH#36148 require tzawareness compat as of 2.0
159+
ts[naive : datetime(1990, 1, 1, 7)]
163160

164161
result = ts.copy()
165-
with tm.assert_produces_warning(FutureWarning):
166-
# GH#36148 will require tzawareness compat
167-
result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = 0
168-
with tm.assert_produces_warning(FutureWarning):
169-
# GH#36148 will require tzawareness compat
170-
result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = ts[4:8]
162+
with pytest.raises(TypeError, match=msg):
163+
# GH#36148 require tzawareness compat as of 2.0
164+
result[naive : datetime(1990, 1, 1, 7)] = 0
165+
with pytest.raises(TypeError, match=msg):
166+
# GH#36148 require tzawareness compat as of 2.0
167+
result[naive : datetime(1990, 1, 1, 7)] = 99
168+
# the __setitems__ here failed, so result should still match ts
171169
tm.assert_series_equal(result, ts)
172170

173-
lb = datetime(1990, 1, 1, 4)
171+
lb = naive
174172
rb = datetime(1990, 1, 1, 7)
175173
msg = r"Invalid comparison between dtype=datetime64\[ns, US/Eastern\] and datetime"
176174
with pytest.raises(TypeError, match=msg):
177175
# tznaive vs tzaware comparison is invalid
178176
# see GH#18376, GH#18162
179177
ts[(ts.index >= lb) & (ts.index <= rb)]
180178

181-
lb = Timestamp(datetime(1990, 1, 1, 4)).tz_localize(rng.tzinfo)
179+
lb = Timestamp(naive).tz_localize(rng.tzinfo)
182180
rb = Timestamp(datetime(1990, 1, 1, 7)).tz_localize(rng.tzinfo)
183181
result = ts[(ts.index >= lb) & (ts.index <= rb)]
184182
expected = ts[4:8]

pandas/tests/series/methods/test_truncate.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from datetime import datetime
22

3+
import pytest
4+
35
import pandas as pd
46
from pandas import (
57
Series,
@@ -13,8 +15,8 @@ def test_truncate_datetimeindex_tz(self):
1315
# GH 9243
1416
idx = date_range("4/1/2005", "4/30/2005", freq="D", tz="US/Pacific")
1517
s = Series(range(len(idx)), index=idx)
16-
with tm.assert_produces_warning(FutureWarning):
17-
# GH#36148 in the future will require tzawareness compat
18+
with pytest.raises(TypeError, match="Cannot compare tz-naive"):
19+
# GH#36148 as of 2.0 we require tzawareness compat
1820
s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4))
1921

2022
lb = idx[1]

0 commit comments

Comments
 (0)