Skip to content

Commit de6f6b3

Browse files
authored
DEPR: silent string casting in binary Timedelta operations (#62989)
1 parent 844e1e1 commit de6f6b3

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ Other Deprecations
807807
- Deprecated option "future.no_silent_downcasting", as it is no longer used. In a future version accessing this option will raise (:issue:`59502`)
808808
- Deprecated passing non-Index types to :meth:`Index.join`; explicitly convert to Index first (:issue:`62897`)
809809
- Deprecated silent casting of non-datetime 'other' to datetime in :meth:`Series.combine_first` (:issue:`62931`)
810+
- Deprecated silently casting strings to :class:`Timedelta` in binary operations with :class:`Timedelta` (:issue:`59653`)
810811
- Deprecated slicing on a :class:`Series` or :class:`DataFrame` with a :class:`DatetimeIndex` using a ``datetime.date`` object, explicitly cast to :class:`Timestamp` instead (:issue:`35830`)
811812
- Deprecated support for the Dataframe Interchange Protocol (:issue:`56732`)
812813
- Deprecated the 'inplace' keyword from :meth:`Resampler.interpolate`, as passing ``True`` raises ``AttributeError`` (:issue:`58690`)

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ def _binary_op_method_timedeltalike(op, name):
798798
return NotImplemented
799799

800800
try:
801-
other = Timedelta(other)
801+
other = _wrapped_to_timedelta(other)
802802
except ValueError:
803803
# failed to parse as timedelta
804804
return NotImplemented
@@ -2341,7 +2341,7 @@ class Timedelta(_Timedelta):
23412341
def __truediv__(self, other):
23422342
if _should_cast_to_timedelta(other):
23432343
# We interpret NaT as timedelta64("NaT")
2344-
other = Timedelta(other)
2344+
other = _wrapped_to_timedelta(other)
23452345
if other is NaT:
23462346
return np.nan
23472347
if other._creso != self._creso:
@@ -2374,7 +2374,7 @@ class Timedelta(_Timedelta):
23742374
def __rtruediv__(self, other):
23752375
if _should_cast_to_timedelta(other):
23762376
# We interpret NaT as timedelta64("NaT")
2377-
other = Timedelta(other)
2377+
other = _wrapped_to_timedelta(other)
23782378
if other is NaT:
23792379
return np.nan
23802380
if self._creso != other._creso:
@@ -2402,7 +2402,7 @@ class Timedelta(_Timedelta):
24022402
# just defer
24032403
if _should_cast_to_timedelta(other):
24042404
# We interpret NaT as timedelta64("NaT")
2405-
other = Timedelta(other)
2405+
other = _wrapped_to_timedelta(other)
24062406
if other is NaT:
24072407
return np.nan
24082408
if self._creso != other._creso:
@@ -2457,7 +2457,7 @@ class Timedelta(_Timedelta):
24572457
# just defer
24582458
if _should_cast_to_timedelta(other):
24592459
# We interpret NaT as timedelta64("NaT")
2460-
other = Timedelta(other)
2460+
other = _wrapped_to_timedelta(other)
24612461
if other is NaT:
24622462
return np.nan
24632463
if self._creso != other._creso:
@@ -2525,6 +2525,7 @@ def truediv_object_array(ndarray left, ndarray right):
25252525
if cnp.get_timedelta64_value(td64) == NPY_NAT:
25262526
# td here should be interpreted as a td64 NaT
25272527
if _should_cast_to_timedelta(obj):
2528+
_wrapped_to_timedelta(obj) # deprecate if allowing string
25282529
res_value = np.nan
25292530
else:
25302531
# if its a number then let numpy handle division, otherwise
@@ -2554,6 +2555,7 @@ def floordiv_object_array(ndarray left, ndarray right):
25542555
if cnp.get_timedelta64_value(td64) == NPY_NAT:
25552556
# td here should be interpreted as a td64 NaT
25562557
if _should_cast_to_timedelta(obj):
2558+
_wrapped_to_timedelta(obj) # deprecate allowing string
25572559
res_value = np.nan
25582560
else:
25592561
# if its a number then let numpy handle division, otherwise
@@ -2585,6 +2587,23 @@ cdef bint is_any_td_scalar(object obj):
25852587
)
25862588

25872589

2590+
cdef inline _wrapped_to_timedelta(object other):
2591+
# Helper for deprecating cases where we cast str to Timedelta
2592+
td = Timedelta(other)
2593+
if isinstance(other, str):
2594+
from pandas.errors import Pandas4Warning
2595+
warnings.warn(
2596+
# GH#59653
2597+
"Scalar operations between Timedelta and string are "
2598+
"deprecated and will raise in a future version. "
2599+
"Explicitly cast to Timedelta first.",
2600+
Pandas4Warning,
2601+
stacklevel=find_stack_level(),
2602+
)
2603+
# When this is enforced, remove str from _should_cast_to_timedelta
2604+
return td
2605+
2606+
25882607
cdef bint _should_cast_to_timedelta(object obj):
25892608
"""
25902609
Should we treat this object as a Timedelta for the purpose of a binary op

pandas/tests/scalar/timedelta/test_arithmetic.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import numpy as np
1212
import pytest
1313

14-
from pandas.errors import OutOfBoundsTimedelta
14+
from pandas.errors import (
15+
OutOfBoundsTimedelta,
16+
Pandas4Warning,
17+
)
1518

1619
import pandas as pd
1720
from pandas import (
@@ -1182,3 +1185,45 @@ def test_ops_error_str():
11821185

11831186
assert not left == right
11841187
assert left != right
1188+
1189+
1190+
@pytest.mark.parametrize("box", [True, False])
1191+
def test_ops_str_deprecated(box):
1192+
# GH#59653
1193+
td = Timedelta("1 day")
1194+
item = "1"
1195+
if box:
1196+
item = np.array([item], dtype=object)
1197+
1198+
msg = "Scalar operations between Timedelta and string are deprecated"
1199+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1200+
td + item
1201+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1202+
item + td
1203+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1204+
td - item
1205+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1206+
item - td
1207+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1208+
item / td
1209+
if not box:
1210+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1211+
td / item
1212+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1213+
item // td
1214+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
1215+
td // item
1216+
else:
1217+
msg = "|".join(
1218+
[
1219+
"ufunc 'divide' cannot use operands",
1220+
"Invalid dtype object for __floordiv__",
1221+
r"unsupported operand type\(s\) for /: 'int' and 'str'",
1222+
]
1223+
)
1224+
with pytest.raises(TypeError, match=msg):
1225+
td / item
1226+
with pytest.raises(TypeError, match=msg):
1227+
item // td
1228+
with pytest.raises(TypeError, match=msg):
1229+
td // item

0 commit comments

Comments
 (0)