Repository: pandas-dev/pandas-stubs (language: python)

Issue:
BUG: Type stubs only support pd.Timedelta, but could (should?) also support datetime.timedelta
### Pandas version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [ ] I have confirmed this bug exists on the [main branch](https://pandas.pydata.org/docs/dev/getting_started/install.html#installing-the-development-version-of-pandas) of pandas.


### Reproducible Example

`typ.py`:

```python
from datetime import timedelta

import pandas as pd


s1 = pd.date_range("2023-01-01", periods=10, freq='1D')

s2 = s1 - timedelta(days=1)

print(s2)
```

Output, showing the expected result:

```none
DatetimeIndex(['2022-12-31', '2023-01-01', '2023-01-02', '2023-01-03',
               '2023-01-04', '2023-01-05', '2023-01-06', '2023-01-07',
               '2023-01-08', '2023-01-09'],
              dtype='datetime64[ns]', freq='D')
```

Mypy config in `pyproject.toml`:

```toml
[tool.mypy]

python_version = '3.11'

strict = true

plugins = [
  'numpy.typing.mypy_plugin',
  'pydantic.mypy',
]
```


### Issue Description

The type stubs don't recognize `datetime.timedelta` as a valid operand:

```
% mypy typ.py
typ.py:7: error: No overload variant of "__sub__" of "DatetimeIndex" matches argument type "timedelta"  [operator]
typ.py:7: note: Possible overload variants:
typ.py:7: note:     def __sub__(self, TimedeltaSeries, /) -> TimestampSeries
typ.py:7: note:     def __sub__(self, Timedelta | TimedeltaIndex, /) -> DatetimeIndex
typ.py:7: note:     def __sub__(self, Timestamp | DatetimeIndex, /) -> TimedeltaIndex
```

### Expected Behavior

I expected this to pass type-checking, because it works fine at runtime and is officially supported behavior (as far as I know).

If this isn't meant to be supported and only works by coincidence, then I apologize for the noise and feel free to close.

I did notice that `pd.Timedelta` is a subclass of `datetime.timedelta`, so this should be a really easy change.

I looked around for a while and didn't find the type stubs for this particular method, otherwise I would have just submitted a patch straight away.

### Installed Versions

<details>

```
% mypy --version
mypy 1.7.1 (compiled: yes)
```

```none
Python 3.11.6 (main, Oct  2 2023, 17:46:45) [Clang 13.1.6 (clang-1316.0.21.2.5)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.18.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import pandas as pd

In [2]: pd.show_versions()

INSTALLED VERSIONS
------------------
commit              : a671b5a8bf5dd13fb19f0e88edc679bc9e15c673
python              : 3.11.6.final.0
python-bits         : 64
OS                  : Darwin
OS-release          : 21.6.0
Version             : Darwin Kernel Version 21.6.0: Fri Sep 15 16:17:04 PDT 2023; root:xnu-8020.240.18.703.5~1/RELEASE_ARM64_T6000
machine             : arm64
processor           : arm
byteorder           : little
LC_ALL              : en_US.UTF-8
LANG                : en_US.UTF-8
LOCALE              : en_US.UTF-8

pandas              : 2.1.4
numpy               : 1.26.2
pytz                : 2023.3.post1
dateutil            : 2.8.2
setuptools          : 68.2.2
pip                 : 23.3.1
Cython              : None
pytest              : 7.4.3
hypothesis          : 6.92.1
sphinx              : None
blosc               : None
feather             : None
xlsxwriter          : None
lxml.etree          : None
html5lib            : None
pymysql             : None
psycopg2            : None
jinja2              : None
IPython             : 8.18.1
pandas_datareader   : None
bs4                 : None
bottleneck          : None
dataframe-api-compat: None
fastparquet         : None
fsspec              : 2023.12.2
gcsfs               : None
matplotlib          : None
numba               : None
numexpr             : None
odfpy               : None
openpyxl            : None
pandas_gbq          : None
pyarrow             : 14.0.2
pyreadstat          : None
pyxlsb              : None
s3fs                : 2023.12.2
scipy               : None
sqlalchemy          : 1.4.50
tables              : None
tabulate            : None
xarray              : None
xlrd                : None
zstandard           : None
tzdata              : 2023.3
qtpy                : None
pyqt5               : None
```

</details>


Discussion / hints:
Thanks for the report.  In `pandas-stubs/core/indexes/datetimes.pyi`, have to change the arithmetic methods that accept `Timedelta` as an argument to use `timedelta` instead, since `pd.Timedelta` is a subclass of `datetime.timedelta`.

PR with tests welcome


Fix patch:
diff --git a/pandas-stubs/_libs/tslibs/timedeltas.pyi b/pandas-stubs/_libs/tslibs/timedeltas.pyi
index 9a71cfc07..51dedb581 100644
--- a/pandas-stubs/_libs/tslibs/timedeltas.pyi
+++ b/pandas-stubs/_libs/tslibs/timedeltas.pyi
@@ -210,7 +210,7 @@ class Timedelta(timedelta):
     @overload
     def __rsub__(self, other: timedelta | Timedelta | np.timedelta64) -> Timedelta: ...
     @overload
-    def __rsub__(self, other: Timestamp | np.datetime64) -> Timestamp: ...
+    def __rsub__(self, other: dt.datetime | Timestamp | np.datetime64) -> Timestamp: ...  # type: ignore[misc]
     @overload
     def __rsub__(self, other: NaTType) -> NaTType: ...
     @overload
diff --git a/pandas-stubs/core/frame.pyi b/pandas-stubs/core/frame.pyi
index f413e6110..759c06dec 100644
--- a/pandas-stubs/core/frame.pyi
+++ b/pandas-stubs/core/frame.pyi
@@ -7,8 +7,7 @@ from collections.abc import (
     MutableMapping,
     Sequence,
 )
-import datetime
-import datetime as _dt
+import datetime as dt
 from re import Pattern
 from typing import (
     Any,
@@ -373,7 +372,7 @@ class DataFrame(NDFrame, OpsMixin):
         convert_dates: dict[HashableT1, StataDateFormat] | None = ...,
         write_index: _bool = ...,
         byteorder: Literal["<", ">", "little", "big"] | None = ...,
-        time_stamp: _dt.datetime | None = ...,
+        time_stamp: dt.datetime | None = ...,
         data_label: _str | None = ...,
         variable_labels: dict[HashableT2, str] | None = ...,
         version: Literal[114, 117, 118, 119] | None = ...,
@@ -1565,14 +1564,14 @@ class DataFrame(NDFrame, OpsMixin):
     ) -> DataFrame: ...
     def at_time(
         self,
-        time: _str | datetime.time,
+        time: _str | dt.time,
         asof: _bool = ...,
         axis: Axis | None = ...,
     ) -> DataFrame: ...
     def between_time(
         self,
-        start_time: _str | datetime.time,
-        end_time: _str | datetime.time,
+        start_time: _str | dt.time,
+        end_time: _str | dt.time,
         axis: Axis | None = ...,
     ) -> DataFrame: ...
     @overload
@@ -1941,7 +1940,7 @@ class DataFrame(NDFrame, OpsMixin):
         level: Level | None = ...,
         origin: Timestamp
         | Literal["epoch", "start", "start_day", "end", "end_day"] = ...,
-        offset: Timedelta | _str | None = ...,
+        offset: dt.timedelta | Timedelta | _str | None = ...,
         group_keys: _bool = ...,
     ) -> Resampler[DataFrame]: ...
     def rfloordiv(
@@ -1968,7 +1967,7 @@ class DataFrame(NDFrame, OpsMixin):
     @overload
     def rolling(
         self,
-        window: int | str | _dt.timedelta | BaseOffset | BaseIndexer,
+        window: int | str | dt.timedelta | BaseOffset | BaseIndexer,
         min_periods: int | None = ...,
         center: _bool = ...,
         on: Hashable | None = ...,
@@ -1982,7 +1981,7 @@ class DataFrame(NDFrame, OpsMixin):
     @overload
     def rolling(
         self,
-        window: int | str | _dt.timedelta | BaseOffset | BaseIndexer,
+        window: int | str | dt.timedelta | BaseOffset | BaseIndexer,
         min_periods: int | None = ...,
         center: _bool = ...,
         on: Hashable | None = ...,
@@ -2217,8 +2216,8 @@ class DataFrame(NDFrame, OpsMixin):
     ) -> DataFrame: ...
     def truncate(
         self,
-        before: datetime.date | _str | int | None = ...,
-        after: datetime.date | _str | int | None = ...,
+        before: dt.date | _str | int | None = ...,
+        after: dt.date | _str | int | None = ...,
         axis: Axis | None = ...,
         copy: _bool = ...,
     ) -> DataFrame: ...
diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi
index 38284618d..1accd842f 100644
--- a/pandas-stubs/core/indexes/accessors.pyi
+++ b/pandas-stubs/core/indexes/accessors.pyi
@@ -1,5 +1,8 @@
 import datetime as dt
-from datetime import tzinfo
+from datetime import (
+    timedelta,
+    tzinfo,
+)
 from typing import (
     Generic,
     Literal,
@@ -166,6 +169,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]):
         freq: str | BaseOffset | None,
         ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ...,
         nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"]
+        | timedelta
         | Timedelta = ...,
     ) -> _DTRoundingMethodReturnType: ...
     def floor(
@@ -173,6 +177,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]):
         freq: str | BaseOffset | None,
         ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ...,
         nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"]
+        | timedelta
         | Timedelta = ...,
     ) -> _DTRoundingMethodReturnType: ...
     def ceil(
@@ -180,6 +185,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]):
         freq: str | BaseOffset | None,
         ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ...,
         nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"]
+        | timedelta
         | Timedelta = ...,
     ) -> _DTRoundingMethodReturnType: ...
 
@@ -206,6 +212,7 @@ class _DatetimeLikeNoTZMethods(
         tz: tzinfo | str | None,
         ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ...,
         nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"]
+        | timedelta
         | Timedelta = ...,
     ) -> _DTNormalizeReturnType: ...
     def tz_convert(self, tz: tzinfo | str | None) -> _DTNormalizeReturnType: ...
diff --git a/pandas-stubs/core/indexes/datetimes.pyi b/pandas-stubs/core/indexes/datetimes.pyi
index 43ee557c7..57ae07935 100644
--- a/pandas-stubs/core/indexes/datetimes.pyi
+++ b/pandas-stubs/core/indexes/datetimes.pyi
@@ -3,6 +3,7 @@ from collections.abc import (
     Sequence,
 )
 from datetime import (
+    datetime,
     timedelta,
     tzinfo,
 )
@@ -59,13 +60,19 @@ class DatetimeIndex(DatetimeTimedeltaMixin[Timestamp], DatetimeIndexProperties):
     @overload
     def __add__(self, other: TimedeltaSeries) -> TimestampSeries: ...
     @overload
-    def __add__(self, other: Timedelta | TimedeltaIndex) -> DatetimeIndex: ...
+    def __add__(
+        self, other: timedelta | Timedelta | TimedeltaIndex
+    ) -> DatetimeIndex: ...
     @overload
     def __sub__(self, other: TimedeltaSeries) -> TimestampSeries: ...
     @overload
-    def __sub__(self, other: Timedelta | TimedeltaIndex) -> DatetimeIndex: ...
+    def __sub__(
+        self, other: timedelta | Timedelta | TimedeltaIndex
+    ) -> DatetimeIndex: ...
     @overload
-    def __sub__(self, other: Timestamp | DatetimeIndex) -> TimedeltaIndex: ...
+    def __sub__(
+        self, other: datetime | Timestamp | DatetimeIndex
+    ) -> TimedeltaIndex: ...
     def to_series(self, index=..., name=...) -> TimestampSeries: ...
     def snap(self, freq: str = ...): ...
     def get_value(self, series, key): ...
diff --git a/pandas-stubs/core/indexes/timedeltas.pyi b/pandas-stubs/core/indexes/timedeltas.pyi
index e97824395..2f44f851e 100644
--- a/pandas-stubs/core/indexes/timedeltas.pyi
+++ b/pandas-stubs/core/indexes/timedeltas.pyi
@@ -52,9 +52,9 @@ class TimedeltaIndex(DatetimeTimedeltaMixin[Timedelta], TimedeltaIndexProperties
     @overload
     def __add__(self, other: DatetimeIndex) -> DatetimeIndex: ...
     @overload
-    def __add__(self, other: Timedelta | Self) -> Self: ...
-    def __radd__(self, other: Timestamp | DatetimeIndex) -> DatetimeIndex: ...  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
-    def __sub__(self, other: Timedelta | Self) -> Self: ...
+    def __add__(self, other: dt.timedelta | Timedelta | Self) -> Self: ...
+    def __radd__(self, other: dt.datetime | Timestamp | DatetimeIndex) -> DatetimeIndex: ...  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
+    def __sub__(self, other: dt.timedelta | Timedelta | Self) -> Self: ...
     def __mul__(self, other: num) -> Self: ...
     @overload  # type: ignore[override]
     def __truediv__(self, other: num | Sequence[float]) -> Self: ...
diff --git a/pandas-stubs/core/reshape/merge.pyi b/pandas-stubs/core/reshape/merge.pyi
index 71d8651db..d80d3ef7a 100644
--- a/pandas-stubs/core/reshape/merge.pyi
+++ b/pandas-stubs/core/reshape/merge.pyi
@@ -1,3 +1,4 @@
+from datetime import timedelta
 from typing import (
     Literal,
     overload,
@@ -6,9 +7,9 @@ from typing import (
 from pandas import (
     DataFrame,
     Series,
+    Timedelta,
 )
 
-from pandas._libs.tslibs import Timedelta
 from pandas._typing import (
     AnyArrayLike,
     HashableT,
@@ -99,7 +100,7 @@ def merge_asof(
     | tuple[str, str]
     | tuple[None, str]
     | tuple[str, None] = ...,
-    tolerance: int | Timedelta | None = ...,
+    tolerance: int | timedelta | Timedelta | None = ...,
     allow_exact_matches: bool = ...,
     direction: Literal["backward", "forward", "nearest"] = ...,
 ) -> DataFrame: ...
diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi
index 2a42e8a3d..407389794 100644
--- a/pandas-stubs/core/series.pyi
+++ b/pandas-stubs/core/series.pyi
@@ -1352,9 +1352,10 @@ class Series(IndexOpsMixin[S1], NDFrame):
         base: int = ...,
         on: _str | None = ...,
         level: Level | None = ...,
-        origin: Timestamp
+        origin: datetime
+        | Timestamp
         | Literal["epoch", "start", "start_day", "end", "end_day"] = ...,
-        offset: Timedelta | _str | None = ...,
+        offset: timedelta | Timedelta | _str | None = ...,
     ) -> Resampler[Series]: ...
     def first(self, offset) -> Series[S1]: ...
     def last(self, offset) -> Series[S1]: ...
@@ -1456,7 +1457,8 @@ class Series(IndexOpsMixin[S1], NDFrame):
     def __add__(self, other: S1 | Self) -> Self: ...
     @overload
     def __add__(
-        self, other: num | _str | Timedelta | _ListLike | Series | np.timedelta64
+        self,
+        other: num | _str | timedelta | Timedelta | _ListLike | Series | np.timedelta64,
     ) -> Series: ...
     # ignore needed for mypy as we want different results based on the arguments
     @overload  # type: ignore[override]
@@ -1485,7 +1487,7 @@ class Series(IndexOpsMixin[S1], NDFrame):
     ) -> Series[_bool]: ...
     @overload
     def __mul__(
-        self, other: Timedelta | TimedeltaSeries | np.timedelta64
+        self, other: timedelta | Timedelta | TimedeltaSeries | np.timedelta64
     ) -> TimedeltaSeries: ...
     @overload
     def __mul__(self, other: num | _ListLike | Series) -> Series: ...
@@ -2043,18 +2045,23 @@ class TimedeltaSeries(Series[Timedelta]):
     def __add__(self, other: Period) -> PeriodSeries: ...
     @overload
     def __add__(
-        self, other: Timestamp | TimestampSeries | DatetimeIndex
+        self, other: datetime | Timestamp | TimestampSeries | DatetimeIndex
     ) -> TimestampSeries: ...
     @overload
     def __add__(  # pyright: ignore[reportIncompatibleMethodOverride]
-        self, other: Timedelta | np.timedelta64
+        self, other: timedelta | Timedelta | np.timedelta64
     ) -> TimedeltaSeries: ...
-    def __radd__(self, other: Timestamp | TimestampSeries) -> TimestampSeries: ...  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
+    def __radd__(self, other: datetime | Timestamp | TimestampSeries) -> TimestampSeries: ...  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
     def __mul__(  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
         self, other: num | Sequence[num] | Series[int] | Series[float]
     ) -> TimedeltaSeries: ...
     def __sub__(  # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
-        self, other: Timedelta | TimedeltaSeries | TimedeltaIndex | np.timedelta64
+        self,
+        other: timedelta
+        | Timedelta
+        | TimedeltaSeries
+        | TimedeltaIndex
+        | np.timedelta64,
     ) -> TimedeltaSeries: ...
     @overload  # type: ignore[override]
     def __truediv__(self, other: float | Sequence[float]) -> Self: ...
diff --git a/pandas-stubs/tseries/holiday.pyi b/pandas-stubs/tseries/holiday.pyi
index 4716fc666..84e63067c 100644
--- a/pandas-stubs/tseries/holiday.pyi
+++ b/pandas-stubs/tseries/holiday.pyi
@@ -71,9 +71,9 @@ def register(cls: type[AbstractHolidayCalendar]) -> None: ...
 def get_calendar(name: str) -> AbstractHolidayCalendar: ...
 
 class AbstractHolidayCalendar:
-    rules: list[Holiday] = ...
-    start_date: Timestamp = ...
-    end_date: Timestamp = ...
+    rules: list[Holiday]
+    start_date: Timestamp
+    end_date: Timestamp
 
     def __init__(self, name: str = "", rules: list[Holiday] | None = None) -> None: ...
     def rule_from_name(self, name: str) -> Holiday | None: ...


Test patch:
diff --git a/tests/test_frame.py b/tests/test_frame.py
index e3875432f..8eb232857 100644
--- a/tests/test_frame.py
+++ b/tests/test_frame.py
@@ -1359,6 +1359,7 @@ def test_types_resample() -> None:
     with pytest_warns_bounded(FutureWarning, "'M' is deprecated", lower="2.1.99"):
         df.resample("M", on="date")
     df.resample("20min", origin="epoch", offset=pd.Timedelta(2, "minutes"), on="date")
+    df.resample("20min", origin="epoch", offset=datetime.timedelta(2), on="date")
 
 
 def test_types_to_dict() -> None:
diff --git a/tests/test_indexes.py b/tests/test_indexes.py
index f878ba17b..24d21a091 100644
--- a/tests/test_indexes.py
+++ b/tests/test_indexes.py
@@ -1048,3 +1048,15 @@ def test_timedelta_div() -> None:
         [1] / index  # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues]
         1 // index  # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues]
         [1] // index  # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues]
+
+
+def test_datetime_operators_builtin() -> None:
+    time = pd.date_range("2022-01-01", "2022-01-31", freq="D")
+    check(assert_type(time + dt.timedelta(0), pd.DatetimeIndex), pd.DatetimeIndex)
+    check(assert_type(time - dt.timedelta(0), pd.DatetimeIndex), pd.DatetimeIndex)
+    check(assert_type(time - dt.datetime.now(), pd.TimedeltaIndex), pd.TimedeltaIndex)
+
+    delta = check(assert_type(time - time, pd.TimedeltaIndex), pd.TimedeltaIndex)
+    check(assert_type(delta + dt.timedelta(0), pd.TimedeltaIndex), pd.TimedeltaIndex)
+    check(assert_type(dt.datetime.now() + delta, pd.DatetimeIndex), pd.DatetimeIndex)
+    check(assert_type(delta - dt.timedelta(0), pd.TimedeltaIndex), pd.TimedeltaIndex)
diff --git a/tests/test_pandas.py b/tests/test_pandas.py
index 2ecb7f392..c94257461 100644
--- a/tests/test_pandas.py
+++ b/tests/test_pandas.py
@@ -1412,6 +1412,15 @@ def test_merge_asof() -> None:
         ),
         pd.DataFrame,
     )
+    check(
+        assert_type(
+            pd.merge_asof(
+                trades, quotes, on="time", by="ticker", tolerance=dt.timedelta(1)
+            ),
+            pd.DataFrame,
+        ),
+        pd.DataFrame,
+    )
     check(
         assert_type(
             pd.merge_asof(
diff --git a/tests/test_series.py b/tests/test_series.py
index ba6ae57f7..cf3762214 100644
--- a/tests/test_series.py
+++ b/tests/test_series.py
@@ -994,6 +994,7 @@ def test_types_resample() -> None:
     s.resample("3min").sum()
     # origin and offset params added in 1.1.0 https://pandas.pydata.org/docs/whatsnew/v1.1.0.html
     s.resample("20min", origin="epoch", offset=pd.Timedelta(value=2, unit="minutes"))
+    s.resample("20min", origin=datetime.datetime.now(), offset=datetime.timedelta(1))
 
 
 # set_flags() method added in 1.2.0 https://pandas.pydata.org/docs/whatsnew/v1.2.0.html
@@ -2860,3 +2861,27 @@ def test_round() -> None:
 def test_series_new_empty() -> None:
     # GH 826
     check(assert_type(pd.Series(), "pd.Series[Any]"), pd.Series)
+
+
+def test_timedeltaseries_operators() -> None:
+    series = pd.Series([pd.Timedelta(days=1)])
+    check(
+        assert_type(series + datetime.datetime.now(), TimestampSeries),
+        pd.Series,
+        pd.Timestamp,
+    )
+    check(
+        assert_type(series + datetime.timedelta(1), TimedeltaSeries),
+        pd.Series,
+        pd.Timedelta,
+    )
+    check(
+        assert_type(datetime.datetime.now() + series, TimestampSeries),
+        pd.Series,
+        pd.Timestamp,
+    )
+    check(
+        assert_type(series - datetime.timedelta(1), TimedeltaSeries),
+        pd.Series,
+        pd.Timedelta,
+    )
diff --git a/tests/test_timefuncs.py b/tests/test_timefuncs.py
index 3473f2897..2bfad7d1b 100644
--- a/tests/test_timefuncs.py
+++ b/tests/test_timefuncs.py
@@ -373,7 +373,10 @@ def test_series_dt_accessors() -> None:
         assert_type(s0.dt.tz_localize(None), "TimestampSeries"), pd.Series, pd.Timestamp
     )
     check(
-        assert_type(s0.dt.tz_localize(pytz.UTC), "TimestampSeries"),
+        assert_type(
+            s0.dt.tz_localize(pytz.UTC, nonexistent=dt.timedelta(0)),
+            "TimestampSeries",
+        ),
         pd.Series,
         pd.Timestamp,
     )
@@ -408,9 +411,21 @@ def test_series_dt_accessors() -> None:
     check(assert_type(s0_local.dt.tz, Optional[dt.tzinfo]), dt.tzinfo)
     check(assert_type(s0.dt.normalize(), "TimestampSeries"), pd.Series, pd.Timestamp)
     check(assert_type(s0.dt.strftime("%Y"), "pd.Series[str]"), pd.Series, str)
-    check(assert_type(s0.dt.round("D"), "TimestampSeries"), pd.Series, pd.Timestamp)
-    check(assert_type(s0.dt.floor("D"), "TimestampSeries"), pd.Series, pd.Timestamp)
-    check(assert_type(s0.dt.ceil("D"), "TimestampSeries"), pd.Series, pd.Timestamp)
+    check(
+        assert_type(s0.dt.round("D", nonexistent=dt.timedelta(1)), "TimestampSeries"),
+        pd.Series,
+        pd.Timestamp,
+    )
+    check(
+        assert_type(s0.dt.floor("D", nonexistent=dt.timedelta(1)), "TimestampSeries"),
+        pd.Series,
+        pd.Timestamp,
+    )
+    check(
+        assert_type(s0.dt.ceil("D", nonexistent=dt.timedelta(1)), "TimestampSeries"),
+        pd.Series,
+        pd.Timestamp,
+    )
     check(assert_type(s0.dt.month_name(), "pd.Series[str]"), pd.Series, str)
     check(assert_type(s0.dt.day_name(), "pd.Series[str]"), pd.Series, str)
 
@@ -1234,3 +1249,14 @@ def test_date_range_unit():
         ),
         pd.DatetimeIndex,
     )
+
+
+def test_DatetimeIndex_sub_timedelta() -> None:
+    # GH838
+    check(
+        assert_type(
+            pd.date_range("2023-01-01", periods=10, freq="1D") - dt.timedelta(days=1),
+            "pd.DatetimeIndex",
+        ),
+        pd.DatetimeIndex,
+    )