Skip to content

Commit c6351d7

Browse files
jbrockmendelphofl
authored andcommitted
DEPR: enforce DatetimeArray.astype deprecations (#49235)
* DEPR: enforce DatetimeArray.astype deprecations * whatsnew
1 parent e97004b commit c6351d7

File tree

9 files changed

+80
-159
lines changed

9 files changed

+80
-159
lines changed

doc/source/whatsnew/v2.0.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ Removal of prior version deprecations/changes
158158
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
159159
- Enforced deprecation disallowing passing a timezone-aware :class:`Timestamp` and ``dtype="datetime64[ns]"`` to :class:`Series` or :class:`DataFrame` constructors (:issue:`41555`)
160160
- Enforced deprecation disallowing passing a sequence of timezone-aware values and ``dtype="datetime64[ns]"`` to to :class:`Series` or :class:`DataFrame` constructors (:issue:`41555`)
161+
- Enforced deprecation disallowing using ``.astype`` to convert a ``datetime64[ns]`` :class:`Series`, :class:`DataFrame`, or :class:`DatetimeIndex` to timezone-aware dtype, use ``obj.tz_localize`` or ``ser.dt.tz_localize`` instead (:issue:`39258`)
162+
- Enforced deprecation disallowing using ``.astype`` to convert a timezone-aware :class:`Series`, :class:`DataFrame`, or :class:`DatetimeIndex` to timezone-naive ``datetime64[ns]`` dtype, use ``obj.tz_localize(None)`` or ``obj.tz_convert("UTC").tz_localize(None)`` instead (:issue:`39258`)
161163
- Removed Date parser functions :func:`~pandas.io.date_converters.parse_date_time`,
162164
:func:`~pandas.io.date_converters.parse_date_fields`, :func:`~pandas.io.date_converters.parse_all_fields`
163165
and :func:`~pandas.io.date_converters.generic_parser` (:issue:`24518`)

pandas/core/arrays/datetimes.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,12 @@
5656
from pandas.util._exceptions import find_stack_level
5757
from pandas.util._validators import validate_inclusive
5858

59-
from pandas.core.dtypes.astype import astype_dt64_to_dt64tz
6059
from pandas.core.dtypes.common import (
6160
DT64NS_DTYPE,
6261
INT64_DTYPE,
6362
is_bool_dtype,
6463
is_datetime64_any_dtype,
6564
is_datetime64_dtype,
66-
is_datetime64_ns_dtype,
6765
is_datetime64tz_dtype,
6866
is_dtype_equal,
6967
is_extension_array_dtype,
@@ -660,15 +658,29 @@ def astype(self, dtype, copy: bool = True):
660658
return type(self)._simple_new(res_values, dtype=res_values.dtype)
661659
# TODO: preserve freq?
662660

663-
elif is_datetime64_ns_dtype(dtype):
664-
return astype_dt64_to_dt64tz(self, dtype, copy, via_utc=False)
665-
666661
elif self.tz is not None and isinstance(dtype, DatetimeTZDtype):
667662
# tzaware unit conversion e.g. datetime64[s, UTC]
668663
np_dtype = np.dtype(dtype.str)
669664
res_values = astype_overflowsafe(self._ndarray, np_dtype, copy=copy)
670-
return type(self)._simple_new(res_values, dtype=dtype)
671-
# TODO: preserve freq?
665+
return type(self)._simple_new(res_values, dtype=dtype, freq=self.freq)
666+
667+
elif self.tz is None and isinstance(dtype, DatetimeTZDtype):
668+
# pre-2.0 this did self.tz_localize(dtype.tz), which did not match
669+
# the Series behavior
670+
raise TypeError(
671+
"Cannot use .astype to convert from timezone-naive dtype to "
672+
"timezone-aware dtype. Use obj.tz_localize instead."
673+
)
674+
675+
elif self.tz is not None and is_datetime64_dtype(dtype):
676+
# pre-2.0 behavior for DTA/DTI was
677+
# values.tz_convert("UTC").tz_localize(None), which did not match
678+
# the Series behavior
679+
raise TypeError(
680+
"Cannot use .astype to convert from timezone-aware dtype to "
681+
"timezone-naive dtype. Use obj.tz_localize(None) or "
682+
"obj.tz_convert('UTC').tz_localize(None) instead."
683+
)
672684

673685
elif (
674686
self.tz is None

pandas/core/dtypes/astype.py

+8-86
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
import inspect
88
from typing import (
99
TYPE_CHECKING,
10-
cast,
1110
overload,
1211
)
13-
import warnings
1412

1513
import numpy as np
1614

@@ -27,7 +25,6 @@
2725
IgnoreRaise,
2826
)
2927
from pandas.errors import IntCastingNaNError
30-
from pandas.util._exceptions import find_stack_level
3128

3229
from pandas.core.dtypes.common import (
3330
is_datetime64_dtype,
@@ -39,17 +36,13 @@
3936
pandas_dtype,
4037
)
4138
from pandas.core.dtypes.dtypes import (
42-
DatetimeTZDtype,
4339
ExtensionDtype,
4440
PandasDtype,
4541
)
4642
from pandas.core.dtypes.missing import isna
4743

4844
if TYPE_CHECKING:
49-
from pandas.core.arrays import (
50-
DatetimeArray,
51-
ExtensionArray,
52-
)
45+
from pandas.core.arrays import ExtensionArray
5346

5447

5548
_dtype_obj = np.dtype(object)
@@ -227,7 +220,13 @@ def astype_array(values: ArrayLike, dtype: DtypeObj, copy: bool = False) -> Arra
227220
raise TypeError(msg)
228221

229222
if is_datetime64tz_dtype(dtype) and is_datetime64_dtype(values.dtype):
230-
return astype_dt64_to_dt64tz(values, dtype, copy, via_utc=True)
223+
# Series.astype behavior pre-2.0 did
224+
# values.tz_localize("UTC").tz_convert(dtype.tz)
225+
# which did not match the DTA/DTI behavior.
226+
raise TypeError(
227+
"Cannot use .astype to convert from timezone-naive dtype to "
228+
"timezone-aware dtype. Use ser.dt.tz_localize instead."
229+
)
231230

232231
if is_dtype_equal(values.dtype, dtype):
233232
if copy:
@@ -351,80 +350,3 @@ def astype_td64_unit_conversion(
351350
mask = isna(values)
352351
np.putmask(result, mask, np.nan)
353352
return result
354-
355-
356-
def astype_dt64_to_dt64tz(
357-
values: ArrayLike, dtype: DtypeObj, copy: bool, via_utc: bool = False
358-
) -> DatetimeArray:
359-
# GH#33401 we have inconsistent behaviors between
360-
# Datetimeindex[naive].astype(tzaware)
361-
# Series[dt64].astype(tzaware)
362-
# This collects them in one place to prevent further fragmentation.
363-
364-
from pandas.core.construction import ensure_wrapped_if_datetimelike
365-
366-
values = ensure_wrapped_if_datetimelike(values)
367-
values = cast("DatetimeArray", values)
368-
aware = isinstance(dtype, DatetimeTZDtype)
369-
370-
if via_utc:
371-
# Series.astype behavior
372-
373-
# caller is responsible for checking this
374-
assert values.tz is None and aware
375-
dtype = cast(DatetimeTZDtype, dtype)
376-
377-
if copy:
378-
# this should be the only copy
379-
values = values.copy()
380-
381-
warnings.warn(
382-
"Using .astype to convert from timezone-naive dtype to "
383-
"timezone-aware dtype is deprecated and will raise in a "
384-
"future version. Use ser.dt.tz_localize instead.",
385-
FutureWarning,
386-
stacklevel=find_stack_level(),
387-
)
388-
389-
# GH#33401 this doesn't match DatetimeArray.astype, which
390-
# goes through the `not via_utc` path
391-
return values.tz_localize("UTC").tz_convert(dtype.tz)
392-
393-
else:
394-
# DatetimeArray/DatetimeIndex.astype behavior
395-
if values.tz is None and aware:
396-
dtype = cast(DatetimeTZDtype, dtype)
397-
warnings.warn(
398-
"Using .astype to convert from timezone-naive dtype to "
399-
"timezone-aware dtype is deprecated and will raise in a "
400-
"future version. Use obj.tz_localize instead.",
401-
FutureWarning,
402-
stacklevel=find_stack_level(),
403-
)
404-
405-
return values.tz_localize(dtype.tz)
406-
407-
elif aware:
408-
# GH#18951: datetime64_tz dtype but not equal means different tz
409-
dtype = cast(DatetimeTZDtype, dtype)
410-
result = values.tz_convert(dtype.tz)
411-
if copy:
412-
result = result.copy()
413-
return result
414-
415-
elif values.tz is not None:
416-
warnings.warn(
417-
"Using .astype to convert from timezone-aware dtype to "
418-
"timezone-naive dtype is deprecated and will raise in a "
419-
"future version. Use obj.tz_localize(None) or "
420-
"obj.tz_convert('UTC').tz_localize(None) instead",
421-
FutureWarning,
422-
stacklevel=find_stack_level(),
423-
)
424-
425-
result = values.tz_convert("UTC").tz_localize(None)
426-
if copy:
427-
result = result.copy()
428-
return result
429-
430-
raise NotImplementedError("dtype_equal case should be handled elsewhere")

pandas/tests/arrays/test_datetimes.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,22 @@ def test_astype_copies(self, dtype, other):
364364
ser = pd.Series([1, 2], dtype=dtype)
365365
orig = ser.copy()
366366

367-
warn = None
367+
err = False
368368
if (dtype == "datetime64[ns]") ^ (other == "datetime64[ns]"):
369369
# deprecated in favor of tz_localize
370-
warn = FutureWarning
371-
372-
with tm.assert_produces_warning(warn):
370+
err = True
371+
372+
if err:
373+
if dtype == "datetime64[ns]":
374+
msg = "Use ser.dt.tz_localize instead"
375+
else:
376+
msg = "from timezone-aware dtype to timezone-naive dtype"
377+
with pytest.raises(TypeError, match=msg):
378+
ser.astype(other)
379+
else:
373380
t = ser.astype(other)
374-
t[:] = pd.NaT
375-
tm.assert_series_equal(ser, orig)
381+
t[:] = pd.NaT
382+
tm.assert_series_equal(ser, orig)
376383

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

pandas/tests/frame/methods/test_astype.py

+3-20
Original file line numberDiff line numberDiff line change
@@ -611,27 +611,10 @@ def test_astype_dt64tz(self, timezone_frame):
611611
result = timezone_frame.astype(object)
612612
tm.assert_frame_equal(result, expected)
613613

614-
with tm.assert_produces_warning(FutureWarning):
614+
msg = "Cannot use .astype to convert from timezone-aware dtype to timezone-"
615+
with pytest.raises(TypeError, match=msg):
615616
# dt64tz->dt64 deprecated
616-
result = timezone_frame.astype("datetime64[ns]")
617-
expected = DataFrame(
618-
{
619-
"A": date_range("20130101", periods=3),
620-
"B": (
621-
date_range("20130101", periods=3, tz="US/Eastern")
622-
.tz_convert("UTC")
623-
.tz_localize(None)
624-
),
625-
"C": (
626-
date_range("20130101", periods=3, tz="CET")
627-
.tz_convert("UTC")
628-
.tz_localize(None)
629-
),
630-
}
631-
)
632-
expected.iloc[1, 1] = NaT
633-
expected.iloc[1, 2] = NaT
634-
tm.assert_frame_equal(result, expected)
617+
timezone_frame.astype("datetime64[ns]")
635618

636619
def test_astype_dt64tz_to_str(self, timezone_frame):
637620
# str formatting

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

+10-24
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,14 @@ def test_astype_with_tz(self):
6262

6363
# with tz
6464
rng = date_range("1/1/2000", periods=10, tz="US/Eastern")
65-
with tm.assert_produces_warning(FutureWarning):
65+
msg = "Cannot use .astype to convert from timezone-aware"
66+
with pytest.raises(TypeError, match=msg):
6667
# deprecated
67-
result = rng.astype("datetime64[ns]")
68-
with tm.assert_produces_warning(FutureWarning):
68+
rng.astype("datetime64[ns]")
69+
with pytest.raises(TypeError, match=msg):
6970
# check DatetimeArray while we're here deprecated
7071
rng._data.astype("datetime64[ns]")
7172

72-
expected = (
73-
date_range("1/1/2000", periods=10, tz="US/Eastern")
74-
.tz_convert("UTC")
75-
.tz_localize(None)
76-
)
77-
tm.assert_index_equal(result, expected)
78-
7973
def test_astype_tzaware_to_tzaware(self):
8074
# GH 18951: tz-aware to tz-aware
8175
idx = date_range("20170101", periods=4, tz="US/Pacific")
@@ -88,17 +82,14 @@ def test_astype_tznaive_to_tzaware(self):
8882
# GH 18951: tz-naive to tz-aware
8983
idx = date_range("20170101", periods=4)
9084
idx = idx._with_freq(None) # tz_localize does not preserve freq
91-
with tm.assert_produces_warning(FutureWarning):
85+
msg = "Cannot use .astype to convert from timezone-naive"
86+
with pytest.raises(TypeError, match=msg):
9287
# dt64->dt64tz deprecated
93-
result = idx.astype("datetime64[ns, US/Eastern]")
94-
with tm.assert_produces_warning(FutureWarning):
88+
idx.astype("datetime64[ns, US/Eastern]")
89+
with pytest.raises(TypeError, match=msg):
9590
# dt64->dt64tz deprecated
9691
idx._data.astype("datetime64[ns, US/Eastern]")
9792

98-
expected = date_range("20170101", periods=4, tz="US/Eastern")
99-
expected = expected._with_freq(None)
100-
tm.assert_index_equal(result, expected)
101-
10293
def test_astype_str_nat(self):
10394
# GH 13149, GH 13209
10495
# verify that we are returning NaT as a string (and not unicode)
@@ -171,15 +162,10 @@ def test_astype_datetime64(self):
171162
assert result is idx
172163

173164
idx_tz = DatetimeIndex(["2016-05-16", "NaT", NaT, np.NaN], tz="EST", name="idx")
174-
with tm.assert_produces_warning(FutureWarning):
165+
msg = "Cannot use .astype to convert from timezone-aware"
166+
with pytest.raises(TypeError, match=msg):
175167
# dt64tz->dt64 deprecated
176168
result = idx_tz.astype("datetime64[ns]")
177-
expected = DatetimeIndex(
178-
["2016-05-16 05:00:00", "NaT", "NaT", "NaT"],
179-
dtype="datetime64[ns]",
180-
name="idx",
181-
)
182-
tm.assert_index_equal(result, expected)
183169

184170
def test_astype_object(self):
185171
rng = date_range("1/1/2000", periods=20)

pandas/tests/indexes/test_base.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,9 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
244244
index = index.tz_localize(tz_naive_fixture)
245245
dtype = index.dtype
246246

247-
warn = None if tz_naive_fixture is None else FutureWarning
248-
# astype dt64 -> dt64tz deprecated
247+
# As of 2.0 astype raises on dt64.astype(dt64tz)
248+
err = tz_naive_fixture is not None
249+
msg = "Cannot use .astype to convert from timezone-naive dtype to"
249250

250251
if attr == "asi8":
251252
result = DatetimeIndex(arg).tz_localize(tz_naive_fixture)
@@ -254,11 +255,15 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
254255
tm.assert_index_equal(result, index)
255256

256257
if attr == "asi8":
257-
with tm.assert_produces_warning(warn):
258+
if err:
259+
with pytest.raises(TypeError, match=msg):
260+
DatetimeIndex(arg).astype(dtype)
261+
else:
258262
result = DatetimeIndex(arg).astype(dtype)
263+
tm.assert_index_equal(result, index)
259264
else:
260265
result = klass(arg, dtype=dtype)
261-
tm.assert_index_equal(result, index)
266+
tm.assert_index_equal(result, index)
262267

263268
if attr == "asi8":
264269
result = DatetimeIndex(list(arg)).tz_localize(tz_naive_fixture)
@@ -267,11 +272,15 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
267272
tm.assert_index_equal(result, index)
268273

269274
if attr == "asi8":
270-
with tm.assert_produces_warning(warn):
275+
if err:
276+
with pytest.raises(TypeError, match=msg):
277+
DatetimeIndex(list(arg)).astype(dtype)
278+
else:
271279
result = DatetimeIndex(list(arg)).astype(dtype)
280+
tm.assert_index_equal(result, index)
272281
else:
273282
result = klass(list(arg), dtype=dtype)
274-
tm.assert_index_equal(result, index)
283+
tm.assert_index_equal(result, index)
275284

276285
@pytest.mark.parametrize("attr", ["values", "asi8"])
277286
@pytest.mark.parametrize("klass", [Index, TimedeltaIndex])

pandas/tests/series/methods/test_astype.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,14 @@ def test_astype_datetime64tz(self):
211211
tm.assert_series_equal(result, expected)
212212

213213
# astype - datetime64[ns, tz]
214-
with tm.assert_produces_warning(FutureWarning):
214+
msg = "Cannot use .astype to convert from timezone-naive"
215+
with pytest.raises(TypeError, match=msg):
215216
# dt64->dt64tz astype deprecated
216-
result = Series(ser.values).astype("datetime64[ns, US/Eastern]")
217-
tm.assert_series_equal(result, ser)
217+
Series(ser.values).astype("datetime64[ns, US/Eastern]")
218218

219-
with tm.assert_produces_warning(FutureWarning):
219+
with pytest.raises(TypeError, match=msg):
220220
# dt64->dt64tz astype deprecated
221-
result = Series(ser.values).astype(ser.dtype)
222-
tm.assert_series_equal(result, ser)
221+
Series(ser.values).astype(ser.dtype)
223222

224223
result = ser.astype("datetime64[ns, CET]")
225224
expected = Series(date_range("20130101 06:00:00", periods=3, tz="CET"))

0 commit comments

Comments
 (0)