Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -742,6 +742,7 @@ Other Deprecations
- Deprecated option "future.no_silent_downcasting", as it is no longer used. In a future version accessing this option will raise (:issue:`59502`)
- Deprecated passing non-Index types to :meth:`Index.join`; explicitly convert to Index first (:issue:`62897`)
- Deprecated silent casting of non-datetime 'other' to datetime in :meth:`Series.combine_first` (:issue:`62931`)
- Deprecated silently casting strings to :class:`Timedelta` in binary operations with :class:`Timedelta` (:issue:`59653`)
- 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`)
- Deprecated support for the Dataframe Interchange Protocol (:issue:`56732`)
- Deprecated the 'inplace' keyword from :meth:`Resampler.interpolate`, as passing ``True`` raises ``AttributeError`` (:issue:`58690`)
Expand Down
29 changes: 24 additions & 5 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ def _binary_op_method_timedeltalike(op, name):
return NotImplemented

try:
other = Timedelta(other)
other = _wrapped_to_timedelta(other)
except ValueError:
# failed to parse as timedelta
return NotImplemented
Expand Down Expand Up @@ -2341,7 +2341,7 @@ class Timedelta(_Timedelta):
def __truediv__(self, other):
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
other = _wrapped_to_timedelta(other)
if other is NaT:
return np.nan
if other._creso != self._creso:
Expand Down Expand Up @@ -2374,7 +2374,7 @@ class Timedelta(_Timedelta):
def __rtruediv__(self, other):
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
other = _wrapped_to_timedelta(other)
if other is NaT:
return np.nan
if self._creso != other._creso:
Expand Down Expand Up @@ -2402,7 +2402,7 @@ class Timedelta(_Timedelta):
# just defer
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
other = _wrapped_to_timedelta(other)
if other is NaT:
return np.nan
if self._creso != other._creso:
Expand Down Expand Up @@ -2457,7 +2457,7 @@ class Timedelta(_Timedelta):
# just defer
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
other = _wrapped_to_timedelta(other)
if other is NaT:
return np.nan
if self._creso != other._creso:
Expand Down Expand Up @@ -2525,6 +2525,7 @@ def truediv_object_array(ndarray left, ndarray right):
if cnp.get_timedelta64_value(td64) == NPY_NAT:
# td here should be interpreted as a td64 NaT
if _should_cast_to_timedelta(obj):
_wrapped_to_timedelta(obj) # deprecate if allowing string
res_value = np.nan
else:
# if its a number then let numpy handle division, otherwise
Expand Down Expand Up @@ -2554,6 +2555,7 @@ def floordiv_object_array(ndarray left, ndarray right):
if cnp.get_timedelta64_value(td64) == NPY_NAT:
# td here should be interpreted as a td64 NaT
if _should_cast_to_timedelta(obj):
_wrapped_to_timedelta(obj) # deprecate allowing string
res_value = np.nan
else:
# if its a number then let numpy handle division, otherwise
Expand Down Expand Up @@ -2585,6 +2587,23 @@ cdef bint is_any_td_scalar(object obj):
)


cdef inline _wrapped_to_timedelta(object other):
# Helper for deprecating cases where we cast str to Timedelta
td = Timedelta(other)
if isinstance(other, str):
from pandas.errors import Pandas4Warning
warnings.warn(
# GH#59653
"Scalar operations between Timedelta and string are "
"deprecated and will raise in a future version. "
"Explicitly cast to Timedelta first.",
Pandas4Warning,
stacklevel=find_stack_level(),
)
# When this is enforced, remove str from _should_cast_to_timedelta
return td


cdef bint _should_cast_to_timedelta(object obj):
"""
Should we treat this object as a Timedelta for the purpose of a binary op
Expand Down
47 changes: 46 additions & 1 deletion pandas/tests/scalar/timedelta/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
import numpy as np
import pytest

from pandas.errors import OutOfBoundsTimedelta
from pandas.errors import (
OutOfBoundsTimedelta,
Pandas4Warning,
)

import pandas as pd
from pandas import (
Expand Down Expand Up @@ -1182,3 +1185,45 @@ def test_ops_error_str():

assert not left == right
assert left != right


@pytest.mark.parametrize("box", [True, False])
def test_ops_str_deprecated(box):
# GH#59653
td = Timedelta("1 day")
item = "1"
if box:
item = np.array([item], dtype=object)

msg = "Scalar operations between Timedelta and string are deprecated"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
td + item
with tm.assert_produces_warning(Pandas4Warning, match=msg):
item + td
with tm.assert_produces_warning(Pandas4Warning, match=msg):
td - item
with tm.assert_produces_warning(Pandas4Warning, match=msg):
item - td
with tm.assert_produces_warning(Pandas4Warning, match=msg):
item / td
if not box:
with tm.assert_produces_warning(Pandas4Warning, match=msg):
td / item
with tm.assert_produces_warning(Pandas4Warning, match=msg):
item // td
with tm.assert_produces_warning(Pandas4Warning, match=msg):
td // item
else:
msg = "|".join(
[
"ufunc 'divide' cannot use operands",
"Invalid dtype object for __floordiv__",
r"unsupported operand type\(s\) for /: 'int' and 'str'",
]
)
with pytest.raises(TypeError, match=msg):
td / item
with pytest.raises(TypeError, match=msg):
item // td
with pytest.raises(TypeError, match=msg):
td // item
Loading