Repository: pandas-dev/pandas

Issue:
DEPR: Tick.delta
Tick.delta was implemented back when we only supported nanos.  It will overflow (with OverflowError instead of OutOfBoundsTimedelta) on very-large ticks.  Better to avoid it altogether and tell users to do `pd.Timedelta(tick)`.



Discussion:

Patch:
diff --git a/doc/source/user_guide/timeseries.rst b/doc/source/user_guide/timeseries.rst
index 105d11b67c9bf..0f0e6271d8329 100644
--- a/doc/source/user_guide/timeseries.rst
+++ b/doc/source/user_guide/timeseries.rst
@@ -1770,7 +1770,8 @@ We can instead only resample those groups where we have points as follows:
     def round(t, freq):
         # round a Timestamp to a specified freq
         freq = to_offset(freq)
-        return pd.Timestamp((t.value // freq.delta.value) * freq.delta.value)
+        td = pd.Timedelta(freq)
+        return pd.Timestamp((t.value // td.value) * td.value)
 
     ts.groupby(partial(round, freq="3min")).sum()
 
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 590cddba60f7a..68c4ef34c8f4f 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -438,6 +438,7 @@ Set the following option to opt into the future behavior:
 Other Deprecations
 ^^^^^^^^^^^^^^^^^^
 - Changed :meth:`Timedelta.resolution_string` to return ``h``, ``min``, ``s``, ``ms``, ``us``, and ``ns`` instead of ``H``, ``T``, ``S``, ``L``, ``U``, and ``N``, for compatibility with respective deprecations in frequency aliases (:issue:`52536`)
+- Deprecated :attr:`offsets.Day.delta`, :attr:`offsets.Hour.delta`, :attr:`offsets.Minute.delta`, :attr:`offsets.Second.delta`, :attr:`offsets.Milli.delta`, :attr:`offsets.Micro.delta`, :attr:`offsets.Nano.delta`, use ``pd.Timedelta(obj)`` instead (:issue:`55498`)
 - Deprecated :func:`pandas.api.types.is_interval` and :func:`pandas.api.types.is_period`, use ``isinstance(obj, pd.Interval)`` and ``isinstance(obj, pd.Period)`` instead (:issue:`55264`)
 - Deprecated :func:`pd.core.internals.api.make_block`, use public APIs instead (:issue:`40226`)
 - Deprecated :func:`read_gbq` and :meth:`DataFrame.to_gbq`. Use ``pandas_gbq.read_gbq`` and ``pandas_gbq.to_gbq`` instead https://pandas-gbq.readthedocs.io/en/latest/api.html (:issue:`55525`)
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index b893939969c68..b3788b6003e67 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -913,8 +913,19 @@ cdef class Tick(SingleConstructorOffset):
         # Since cdef classes have no __dict__, we need to override
         return ""
 
+    @cache_readonly
+    def _as_pd_timedelta(self):
+        return Timedelta(self)
+
     @property
     def delta(self):
+        warnings.warn(
+            # GH#55498
+            f"{type(self).__name__}.delta is deprecated and will be removed in "
+            "a future version. Use pd.Timedelta(obj) instead",
+            FutureWarning,
+            stacklevel=find_stack_level(),
+        )
         try:
             return self.n * Timedelta(self._nanos_inc)
         except OverflowError as err:
@@ -962,22 +973,22 @@ cdef class Tick(SingleConstructorOffset):
             except ValueError:
                 # e.g. "infer"
                 return False
-        return self.delta == other
+        return self._as_pd_timedelta == other
 
     def __ne__(self, other):
         return not (self == other)
 
     def __le__(self, other):
-        return self.delta.__le__(other)
+        return self._as_pd_timedelta.__le__(other)
 
     def __lt__(self, other):
-        return self.delta.__lt__(other)
+        return self._as_pd_timedelta.__lt__(other)
 
     def __ge__(self, other):
-        return self.delta.__ge__(other)
+        return self._as_pd_timedelta.__ge__(other)
 
     def __gt__(self, other):
-        return self.delta.__gt__(other)
+        return self._as_pd_timedelta.__gt__(other)
 
     def __mul__(self, other):
         if is_float_object(other):
@@ -997,13 +1008,13 @@ cdef class Tick(SingleConstructorOffset):
     def __truediv__(self, other):
         if not isinstance(self, Tick):
             # cython semantics mean the args are sometimes swapped
-            result = other.delta.__rtruediv__(self)
+            result = other._as_pd_timedelta.__rtruediv__(self)
         else:
-            result = self.delta.__truediv__(other)
+            result = self._as_pd_timedelta.__truediv__(other)
         return _wrap_timedelta_result(result)
 
     def __rtruediv__(self, other):
-        result = self.delta.__rtruediv__(other)
+        result = self._as_pd_timedelta.__rtruediv__(other)
         return _wrap_timedelta_result(result)
 
     def __add__(self, other):
@@ -1011,7 +1022,7 @@ cdef class Tick(SingleConstructorOffset):
             if type(self) is type(other):
                 return type(self)(self.n + other.n)
             else:
-                return delta_to_tick(self.delta + other.delta)
+                return delta_to_tick(self._as_pd_timedelta + other._as_pd_timedelta)
         try:
             return self._apply(other)
         except ApplyTypeError:
@@ -1029,7 +1040,7 @@ cdef class Tick(SingleConstructorOffset):
         # Timestamp can handle tz and nano sec, thus no need to use apply_wraps
         if isinstance(other, _Timestamp):
             # GH#15126
-            return other + self.delta
+            return other + self._as_pd_timedelta
         elif other is NaT:
             return NaT
         elif cnp.is_datetime64_object(other) or PyDate_Check(other):
@@ -1037,7 +1048,7 @@ cdef class Tick(SingleConstructorOffset):
             return Timestamp(other) + self
 
         if cnp.is_timedelta64_object(other) or PyDelta_Check(other):
-            return other + self.delta
+            return other + self._as_pd_timedelta
 
         raise ApplyTypeError(f"Unhandled type: {type(other).__name__}")
 
diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py
index 2b03a64236128..4fc2fdb3202b1 100644
--- a/pandas/core/indexes/datetimelike.py
+++ b/pandas/core/indexes/datetimelike.py
@@ -533,7 +533,7 @@ def _as_range_index(self) -> RangeIndex:
         # Convert our i8 representations to RangeIndex
         # Caller is responsible for checking isinstance(self.freq, Tick)
         freq = cast(Tick, self.freq)
-        tick = freq.delta._value
+        tick = Timedelta(freq).as_unit("ns")._value
         rng = range(self[0]._value, self[-1]._value + tick, tick)
         return RangeIndex(rng)
 
diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py
index 73143730085d6..c978abd8c2427 100644
--- a/pandas/core/indexes/datetimes.py
+++ b/pandas/core/indexes/datetimes.py
@@ -17,6 +17,8 @@
 )
 from pandas._libs.tslibs import (
     Resolution,
+    Tick,
+    Timedelta,
     periods_per_day,
     timezones,
     to_offset,
@@ -393,10 +395,11 @@ def _is_dates_only(self) -> bool:
         -------
         bool
         """
-        delta = getattr(self.freq, "delta", None)
+        if isinstance(self.freq, Tick):
+            delta = Timedelta(self.freq)
 
-        if delta and delta % dt.timedelta(days=1) != dt.timedelta(days=0):
-            return False
+            if delta % dt.timedelta(days=1) != dt.timedelta(days=0):
+                return False
 
         return self._values._is_dates_only
 


Test patch:
diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py
index d3ce0e222e4cd..d8c1786b6b422 100644
--- a/pandas/tests/arithmetic/test_numeric.py
+++ b/pandas/tests/arithmetic/test_numeric.py
@@ -299,6 +299,12 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box_with_array
             expected = expected.astype(dtype)
         elif type(three_days) is timedelta:
             expected = expected.astype("m8[us]")
+        elif isinstance(
+            three_days,
+            (pd.offsets.Day, pd.offsets.Hour, pd.offsets.Minute, pd.offsets.Second),
+        ):
+            # closest reso is Second
+            expected = expected.astype("m8[s]")
 
         index = tm.box_expected(index, box)
         expected = tm.box_expected(expected, box)
diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py
index 363ae1fa9c644..cc1e91893e308 100644
--- a/pandas/tests/scalar/timedelta/test_arithmetic.py
+++ b/pandas/tests/scalar/timedelta/test_arithmetic.py
@@ -1034,7 +1034,7 @@ def test_compare_tick(self, tick_classes):
         cls = tick_classes
 
         off = cls(4)
-        td = off.delta
+        td = off._as_pd_timedelta
         assert isinstance(td, Timedelta)
 
         assert td == off
diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py
index abf187ace7cb3..b68b91826bc6f 100644
--- a/pandas/tests/tseries/offsets/test_ticks.py
+++ b/pandas/tests/tseries/offsets/test_ticks.py
@@ -242,8 +242,10 @@ def test_tick_delta_overflow():
     # GH#55503 raise OutOfBoundsTimedelta, not OverflowError
     tick = offsets.Day(10**9)
     msg = "Cannot cast 1000000000 days 00:00:00 to unit='ns' without overflow"
+    depr_msg = "Day.delta is deprecated"
     with pytest.raises(OutOfBoundsTimedelta, match=msg):
-        tick.delta
+        with tm.assert_produces_warning(FutureWarning, match=depr_msg):
+            tick.delta
 
 
 @pytest.mark.parametrize("cls", tick_classes)
@@ -254,24 +256,24 @@ def test_tick_division(cls):
     assert off / 2 == cls(5)
     assert off / 2.0 == cls(5)
 
-    assert off / off.delta == 1
-    assert off / off.delta.to_timedelta64() == 1
+    assert off / off._as_pd_timedelta == 1
+    assert off / off._as_pd_timedelta.to_timedelta64() == 1
 
-    assert off / Nano(1) == off.delta / Nano(1).delta
+    assert off / Nano(1) == off._as_pd_timedelta / Nano(1)._as_pd_timedelta
 
     if cls is not Nano:
         # A case where we end up with a smaller class
         result = off / 1000
         assert isinstance(result, offsets.Tick)
         assert not isinstance(result, cls)
-        assert result.delta == off.delta / 1000
+        assert result._as_pd_timedelta == off._as_pd_timedelta / 1000
 
     if cls._nanos_inc < Timedelta(seconds=1)._value:
         # Case where we end up with a bigger class
         result = off / 0.001
         assert isinstance(result, offsets.Tick)
         assert not isinstance(result, cls)
-        assert result.delta == off.delta / 0.001
+        assert result._as_pd_timedelta == off._as_pd_timedelta / 0.001
 
 
 def test_tick_mul_float():
@@ -293,7 +295,7 @@ def test_tick_mul_float():
 @pytest.mark.parametrize("cls", tick_classes)
 def test_tick_rdiv(cls):
     off = cls(10)
-    delta = off.delta
+    delta = off._as_pd_timedelta
     td64 = delta.to_timedelta64()
     instance__type = ".".join([cls.__module__, cls.__name__])
     msg = (
@@ -385,7 +387,7 @@ def test_compare_ticks_to_strs(cls):
 def test_compare_ticks_to_timedeltalike(cls):
     off = cls(19)
 
-    td = off.delta
+    td = off._as_pd_timedelta
 
     others = [td, td.to_timedelta64()]
     if cls is not Nano: