Skip to content

Commit f958cf5

Browse files
authored
DEPR: astype dt64<->dt64tz (#39258)
1 parent aa429d4 commit f958cf5

File tree

13 files changed

+134
-23
lines changed

13 files changed

+134
-23
lines changed

doc/source/user_guide/timeseries.rst

+1-8
Original file line numberDiff line numberDiff line change
@@ -2605,17 +2605,10 @@ For example, to localize and convert a naive stamp to time zone aware.
26052605
s_naive.dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
26062606
26072607
Time zone information can also be manipulated using the ``astype`` method.
2608-
This method can localize and convert time zone naive timestamps or
2609-
convert time zone aware timestamps.
2608+
This method can convert between different timezone-aware dtypes.
26102609

26112610
.. ipython:: python
26122611
2613-
# localize and convert a naive time zone
2614-
s_naive.astype("datetime64[ns, US/Eastern]")
2615-
2616-
# make an aware tz naive
2617-
s_aware.astype("datetime64[ns]")
2618-
26192612
# convert to a new time zone
26202613
s_aware.astype("datetime64[ns, CET]")
26212614

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ Deprecations
193193
- Deprecated :attr:`Rolling.win_type` returning ``"freq"`` (:issue:`38963`)
194194
- Deprecated :attr:`Rolling.is_datetimelike` (:issue:`38963`)
195195
- Deprecated :meth:`core.window.ewm.ExponentialMovingWindow.vol` (:issue:`39220`)
196+
- Using ``.astype`` to convert between ``datetime64[ns]`` dtype and :class:`DatetimeTZDtype` is deprecated and will raise in a future version, use ``obj.tz_localize`` or ``obj.dt.tz_localize`` instead (:issue:`38622`)
196197
-
197198

198199
.. ---------------------------------------------------------------------------

pandas/core/dtypes/cast.py

+30
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
tz_compare,
3939
)
4040
from pandas._typing import AnyArrayLike, ArrayLike, Dtype, DtypeObj, Scalar
41+
from pandas.util._exceptions import find_stack_level
4142
from pandas.util._validators import validate_bool_kwarg
4243

4344
from pandas.core.dtypes.common import (
@@ -964,6 +965,16 @@ def astype_dt64_to_dt64tz(
964965
if copy:
965966
# this should be the only copy
966967
values = values.copy()
968+
969+
level = find_stack_level()
970+
warnings.warn(
971+
"Using .astype to convert from timezone-naive dtype to "
972+
"timezone-aware dtype is deprecated and will raise in a "
973+
"future version. Use ser.dt.tz_localize instead.",
974+
FutureWarning,
975+
stacklevel=level,
976+
)
977+
967978
# FIXME: GH#33401 this doesn't match DatetimeArray.astype, which
968979
# goes through the `not via_utc` path
969980
return values.tz_localize("UTC").tz_convert(dtype.tz)
@@ -973,6 +984,15 @@ def astype_dt64_to_dt64tz(
973984

974985
if values.tz is None and aware:
975986
dtype = cast(DatetimeTZDtype, dtype)
987+
level = find_stack_level()
988+
warnings.warn(
989+
"Using .astype to convert from timezone-naive dtype to "
990+
"timezone-aware dtype is deprecated and will raise in a "
991+
"future version. Use obj.tz_localize instead.",
992+
FutureWarning,
993+
stacklevel=level,
994+
)
995+
976996
return values.tz_localize(dtype.tz)
977997

978998
elif aware:
@@ -984,6 +1004,16 @@ def astype_dt64_to_dt64tz(
9841004
return result
9851005

9861006
elif values.tz is not None:
1007+
level = find_stack_level()
1008+
warnings.warn(
1009+
"Using .astype to convert from timezone-aware dtype to "
1010+
"timezone-naive dtype is deprecated and will raise in a "
1011+
"future version. Use obj.tz_localize(None) or "
1012+
"obj.tz_convert('UTC').tz_localize(None) instead",
1013+
FutureWarning,
1014+
stacklevel=level,
1015+
)
1016+
9871017
result = values.tz_convert("UTC").tz_localize(None)
9881018
if copy:
9891019
result = result.copy()

pandas/core/dtypes/dtypes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def _hash_categories(categories, ordered: Ordered = True) -> int:
440440

441441
if DatetimeTZDtype.is_dtype(categories.dtype):
442442
# Avoid future warning.
443-
categories = categories.astype("datetime64[ns]")
443+
categories = categories.view("datetime64[ns]")
444444

445445
cat_array = hash_array(np.asarray(categories), categorize=False)
446446
if ordered:

pandas/core/internals/blocks.py

+3
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,9 @@ def get_values(self, dtype: Optional[Dtype] = None):
21752175
def external_values(self):
21762176
# NB: this is different from np.asarray(self.values), since that
21772177
# return an object-dtype ndarray of Timestamps.
2178+
if self.is_datetimetz:
2179+
# avoid FutureWarning in .astype in casting from dt64t to dt64
2180+
return self.values._data
21782181
return np.asarray(self.values.astype("datetime64[ns]", copy=False))
21792182

21802183
def fillna(self, value, limit=None, inplace=False, downcast=None):

pandas/tests/arrays/test_datetimes.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,18 @@ def test_astype_to_same(self):
175175
)
176176
def test_astype_copies(self, dtype, other):
177177
# https://github.com/pandas-dev/pandas/pull/32490
178-
s = pd.Series([1, 2], dtype=dtype)
179-
orig = s.copy()
180-
t = s.astype(other)
178+
ser = pd.Series([1, 2], dtype=dtype)
179+
orig = ser.copy()
180+
181+
warn = None
182+
if (dtype == "datetime64[ns]") ^ (other == "datetime64[ns]"):
183+
# deprecated in favor of tz_localize
184+
warn = FutureWarning
185+
186+
with tm.assert_produces_warning(warn):
187+
t = ser.astype(other)
181188
t[:] = pd.NaT
182-
tm.assert_series_equal(s, orig)
189+
tm.assert_series_equal(ser, orig)
183190

184191
@pytest.mark.parametrize("dtype", [int, np.int32, np.int64, "uint32", "uint64"])
185192
def test_astype_int(self, dtype):

pandas/tests/frame/methods/test_astype.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,9 @@ def test_astype_dt64tz(self, timezone_frame):
515515
result = timezone_frame.astype(object)
516516
tm.assert_frame_equal(result, expected)
517517

518-
result = timezone_frame.astype("datetime64[ns]")
518+
with tm.assert_produces_warning(FutureWarning):
519+
# dt64tz->dt64 deprecated
520+
result = timezone_frame.astype("datetime64[ns]")
519521
expected = DataFrame(
520522
{
521523
"A": date_range("20130101", periods=3),

pandas/tests/indexes/datetimes/methods/test_astype.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ def test_astype_with_tz(self):
5858

5959
# with tz
6060
rng = date_range("1/1/2000", periods=10, tz="US/Eastern")
61-
result = rng.astype("datetime64[ns]")
61+
with tm.assert_produces_warning(FutureWarning):
62+
# deprecated
63+
result = rng.astype("datetime64[ns]")
64+
with tm.assert_produces_warning(FutureWarning):
65+
# check DatetimeArray while we're here deprecated
66+
rng._data.astype("datetime64[ns]")
67+
6268
expected = (
6369
date_range("1/1/2000", periods=10, tz="US/Eastern")
6470
.tz_convert("UTC")
@@ -78,7 +84,13 @@ def test_astype_tznaive_to_tzaware(self):
7884
# GH 18951: tz-naive to tz-aware
7985
idx = date_range("20170101", periods=4)
8086
idx = idx._with_freq(None) # tz_localize does not preserve freq
81-
result = idx.astype("datetime64[ns, US/Eastern]")
87+
with tm.assert_produces_warning(FutureWarning):
88+
# dt64->dt64tz deprecated
89+
result = idx.astype("datetime64[ns, US/Eastern]")
90+
with tm.assert_produces_warning(FutureWarning):
91+
# dt64->dt64tz deprecated
92+
idx._data.astype("datetime64[ns, US/Eastern]")
93+
8294
expected = date_range("20170101", periods=4, tz="US/Eastern")
8395
expected = expected._with_freq(None)
8496
tm.assert_index_equal(result, expected)
@@ -155,7 +167,9 @@ def test_astype_datetime64(self):
155167
assert result is idx
156168

157169
idx_tz = DatetimeIndex(["2016-05-16", "NaT", NaT, np.NaN], tz="EST", name="idx")
158-
result = idx_tz.astype("datetime64[ns]")
170+
with tm.assert_produces_warning(FutureWarning):
171+
# dt64tz->dt64 deprecated
172+
result = idx_tz.astype("datetime64[ns]")
159173
expected = DatetimeIndex(
160174
["2016-05-16 05:00:00", "NaT", "NaT", "NaT"],
161175
dtype="datetime64[ns]",

pandas/tests/indexes/test_base.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,18 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
349349
index = index.tz_localize(tz_naive_fixture)
350350
dtype = index.dtype
351351

352+
warn = None if tz_naive_fixture is None else FutureWarning
353+
# astype dt64 -> dt64tz deprecated
354+
352355
if attr == "asi8":
353356
result = DatetimeIndex(arg).tz_localize(tz_naive_fixture)
354357
else:
355358
result = klass(arg, tz=tz_naive_fixture)
356359
tm.assert_index_equal(result, index)
357360

358361
if attr == "asi8":
359-
result = DatetimeIndex(arg).astype(dtype)
362+
with tm.assert_produces_warning(warn):
363+
result = DatetimeIndex(arg).astype(dtype)
360364
else:
361365
result = klass(arg, dtype=dtype)
362366
tm.assert_index_equal(result, index)
@@ -368,7 +372,8 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
368372
tm.assert_index_equal(result, index)
369373

370374
if attr == "asi8":
371-
result = DatetimeIndex(list(arg)).astype(dtype)
375+
with tm.assert_produces_warning(warn):
376+
result = DatetimeIndex(list(arg)).astype(dtype)
372377
else:
373378
result = klass(list(arg), dtype=dtype)
374379
tm.assert_index_equal(result, index)

pandas/tests/indexes/test_common.py

+7
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ def test_astype_preserves_name(self, index, dtype):
352352
if dtype in ["int64", "uint64"]:
353353
if needs_i8_conversion(index.dtype):
354354
warn = FutureWarning
355+
elif (
356+
isinstance(index, DatetimeIndex)
357+
and index.tz is not None
358+
and dtype == "datetime64[ns]"
359+
):
360+
# This astype is deprecated in favor of tz_localize
361+
warn = FutureWarning
355362
try:
356363
# Some of these conversions cannot succeed so we use a try / except
357364
with tm.assert_produces_warning(warn, check_stacklevel=False):

pandas/tests/series/methods/test_astype.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,14 @@ def test_astype_datetime64tz(self):
193193
tm.assert_series_equal(result, expected)
194194

195195
# astype - datetime64[ns, tz]
196-
result = Series(s.values).astype("datetime64[ns, US/Eastern]")
196+
with tm.assert_produces_warning(FutureWarning):
197+
# dt64->dt64tz astype deprecated
198+
result = Series(s.values).astype("datetime64[ns, US/Eastern]")
197199
tm.assert_series_equal(result, s)
198200

199-
result = Series(s.values).astype(s.dtype)
201+
with tm.assert_produces_warning(FutureWarning):
202+
# dt64->dt64tz astype deprecated
203+
result = Series(s.values).astype(s.dtype)
200204
tm.assert_series_equal(result, s)
201205

202206
result = s.astype("datetime64[ns, CET]")

pandas/tests/series/methods/test_convert_dtypes.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,18 @@ class TestSeriesConvertDtypes:
156156
def test_convert_dtypes(
157157
self, data, maindtype, params, expected_default, expected_other
158158
):
159+
warn = None
160+
if (
161+
hasattr(data, "dtype")
162+
and data.dtype == "M8[ns]"
163+
and isinstance(maindtype, pd.DatetimeTZDtype)
164+
):
165+
# this astype is deprecated in favor of tz_localize
166+
warn = FutureWarning
167+
159168
if maindtype is not None:
160-
series = pd.Series(data, dtype=maindtype)
169+
with tm.assert_produces_warning(warn):
170+
series = pd.Series(data, dtype=maindtype)
161171
else:
162172
series = pd.Series(data)
163173

@@ -177,7 +187,17 @@ def test_convert_dtypes(
177187
if all(params_dict[key] is val for key, val in zip(spec[::2], spec[1::2])):
178188
expected_dtype = dtype
179189

180-
expected = pd.Series(data, dtype=expected_dtype)
190+
warn2 = None
191+
if (
192+
hasattr(data, "dtype")
193+
and data.dtype == "M8[ns]"
194+
and isinstance(expected_dtype, pd.DatetimeTZDtype)
195+
):
196+
# this astype is deprecated in favor of tz_localize
197+
warn2 = FutureWarning
198+
199+
with tm.assert_produces_warning(warn2):
200+
expected = pd.Series(data, dtype=expected_dtype)
181201
tm.assert_series_equal(result, expected)
182202

183203
# Test that it is a copy

pandas/util/_exceptions.py

+25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import inspect
23
from typing import Tuple
34

45

@@ -17,3 +18,27 @@ def rewrite_exception(old_name: str, new_name: str):
1718
args = args + err.args[1:]
1819
err.args = args
1920
raise
21+
22+
23+
def find_stack_level() -> int:
24+
"""
25+
Find the appropriate stacklevel with which to issue a warning for astype.
26+
"""
27+
stack = inspect.stack()
28+
29+
# find the lowest-level "astype" call that got us here
30+
for n in range(2, 6):
31+
if stack[n].function == "astype":
32+
break
33+
34+
while stack[n].function in ["astype", "apply", "_astype"]:
35+
# e.g.
36+
# bump up Block.astype -> BlockManager.astype -> NDFrame.astype
37+
# bump up Datetime.Array.astype -> DatetimeIndex.astype
38+
n += 1
39+
40+
if stack[n].function == "__init__":
41+
# Series.__init__
42+
n += 1
43+
44+
return n

0 commit comments

Comments
 (0)