DEP: Remove tzdata as a hard dependency outside of Windows

- [x] closes #63264 (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

This may still need more discussion.

Docs change:

<img width="350" height="150" alt="image" src="https://github.com/[USER_REDACTED]nts/assets/639f8900-2a07-4423-82d7-1001ac0a6abc" />

@@ -115,7 +115,7 @@ details, see the commit logs at https://github.com/[USER_REDACTED]ndas-dev/pandas.
 ## Dependencies
 - [NumPy - Adds support for large, multi-dimensional arrays, matrices and high-level mathematical functions to operate on these arrays](https://www.numpy.org)
 - [python-dateutil - Provides powerful extensions to the standard datetime module](https://dateutil.readthedocs.io/en/stable/index.html)
-- [tzdata - Provides an IANA time zone database](https://tzdata.readthedocs.io/en/latest/)
+- [tzdata - Provides an IANA time zone database](https://tzdata.readthedocs.io/en/latest/) (Only required on Windows/Emscripten)

 See the [full installation instructions](https://pandas.pydata.org/pandas-docs/stable/install.html#dependencies) for minimum supported versions of required, recommended and optional dependencies.

@@ -150,9 +150,11 @@ Package                                                          Minimum support
 ================================================================ ==========================
 `NumPy <https://numpy.org>`__                                    1.26.0
 `python-dateutil <https://dateutil.readthedocs.io/en/stable/>`__ 2.8.2
-`tzdata <https://pypi.org/project/tzdata/>`__                    2023.3
+ `tzdata <https://pypi.org/project/tzdata/>`__ \*                /
 ================================================================ ==========================

+\* ``tzdata`` is only required on Windows and Pyodide (Emscripten).
+
 Generally, the minimum supported version is ~2 years old from the release date of a major or minor pandas version.

 .. _install.optional_dependencies:

@@ -657,8 +657,6 @@ The following required dependencies were updated:
 +=================+======================+
 | numpy           | 1.26.0               |
 +-----------------+----------------------+
-| tzdata          | 2023.3               |
-+-----------------+----------------------+

 For `optional libraries <https://pandas.pydata.org/docs/getting_started/install.html>`_ the general recommendation is to use the latest version.
 The following table lists the lowest version per library that is currently being tested throughout the development of pandas.

@@ -59,9 +59,6 @@ cdef bint is_utc_zoneinfo(tzinfo tz):
             utc_zoneinfo = zoneinfo.ZoneInfo("UTC")
         except zoneinfo.ZoneInfoNotFoundError:
             return False
-        # Warn if tzdata is too old, even if there is a system tzdata to alert
-        # users about the mismatch between local/system tzdata
-        import_optional_dependency("tzdata", errors="warn", min_version="2022.7")

     return tz is utc_zoneinfo

@@ -3,6 +3,9 @@
     timedelta,
     timezone,
 )
+import subprocess
+import sys
+import textwrap

 import dateutil.tz
 import pytest
@@ -16,6 +19,25 @@
 from pandas import Timestamp

+[USER_REDACTED].mark.single_cpu
+def test_no_timezone_data():
+    # https://github.com/[USER_REDACTED]ndas-dev/pandas/pull/63335
+    # Test error message when timezone data is not available.
+    msg = "'No time zone found with key Europe/Brussels'"
+    code = textwrap.dedent(
+        f"""\
+        import sys, zoneinfo, pandas as pd
+        sys.modules['tzdata'] = None
+        zoneinfo.reset_tzpath(['/path/to/nowhere'])
+        try:
+            pd.to_datetime('2012-01-01').tz_localize('Europe/Brussels')
+        except zoneinfo.ZoneInfoNotFoundError as err:
+            assert str(err) == "{msg}"
+        """
+    )
+    subprocess.check_call([sys.executable, "-c", code])
+
+
 def test_is_utc(utc_fixture):
     tz = timezones.maybe_get_tz(utc_fixture)
     assert timezones.is_utc(tz)

@@ -29,7 +29,9 @@ requires-python = '>=3.11'
 dependencies = [
   "numpy>=1.26.0",
   "python-dateutil>=2.8.2",
-  "tzdata>=2023.3"
+  "tzdata; sys_platform == 'win32'",
+  # Emscripten is the platform system for Pyodide.
+  "tzdata; sys_platform == 'emscripten'",
 ]
 classifiers = [
     '[NAME_REDACTED] :: 5 - Production/Stable',

This probably will require a change to the `pandas-feedstock` for `conda`

With tests seeming to be fine with no minimum here (ref: https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63264#issuecomment-3629049941), I'm wondering if we should remove the lower bound.

or emscriptem?

I see some other packages use `sys_platform != "emscripten"` instead. Do we know if there is a preference? (I don't directly find a good source about this in the docs)

Interesting, I took it from here.

https://pyodide.org/en/0.16.1/faq.html#how-to-detect-that-code-is-run-with-pyodide

But looking at the latest version of the docs, it now uses `sys.platform`.

https://pyodide.org/en/stable/usage/faq.html#how-to-detect-that-code-is-run-with-pyodide

Will update.

```suggestion
 `tzdata <https://pypi.org/project/tzdata/>`__ \*                /
```


BUG: Add logical operators to pd.col Expression class

## Summary

Add `__and__`, `__rand__`, `__or__`, `__ror__`, `__xor__`, `__rxor__`, `__invert__` methods to `Expression` class to support combining conditions with `&`, `|`, `^`, `~` operators in `pd.col` expressions.

## Example

```python
import pandas as pd
import io

data = '''datetime,cfs
2018-02-28,350
2018-03-15,420
2019-04-10,380
2019-06-01,450
'''
dd = pd.read_csv(io.StringIO(data), parse_dates=['datetime']).set_index('datetime')

# This now works with & operator
result = (dd
 .reset_index()
 .sort_values('datetime')
 .loc[(pd.col('datetime') > '2018-03-01') & (pd.col('datetime') <= '2019-05-31')]
 .assign(cfs=pd.col('cfs').clip(upper=400))
)
```

Closes #63322

@@ -37,6 +37,12 @@
     "__lt__": "<",
     "__eq__": "==",
     "__ne__": "!=",
+    "__and__": "&",
+    "__rand__": "&",
+    "__or__": "|",
+    "__ror__": "|",
+    "__xor__": "^",
+    "__rxor__": "^",
 }

@@ -157,6 +163,28 @@ def __mod__(self, other: Any) -> Expression:
     def __rmod__(self, other: Any) -> Expression:
         return self._with_binary_op("__rmod__", other)

+    # Logical ops
+    def __and__(self, other: Any) -> Expression:
+        return self._with_binary_op("__and__", other)
+
+    def __rand__(self, other: Any) -> Expression:
+        return self._with_binary_op("__rand__", other)
+
+    def __or__(self, other: Any) -> Expression:
+        return self._with_binary_op("__or__", other)
+
+    def __ror__(self, other: Any) -> Expression:
+        return self._with_binary_op("__ror__", other)
+
+    def __xor__(self, other: Any) -> Expression:
+        return self._with_binary_op("__xor__", other)
+
+    def __rxor__(self, other: Any) -> Expression:
+        return self._with_binary_op("__rxor__", other)
+
+    def __invert__(self) -> Expression:
+        return Expression(lambda df: ~self(df), f"(~{self._repr_str})")
+
     def __array_ufunc__(
         self, ufunc: Callable[..., Any], method: str, *inputs: Any, **kwargs: Any
     ) -> Expression:

@@ -97,3 +97,65 @@ def mean(self):
         result = df.assign(b=pd.col("a").xyz.mean())
     expected = pd.DataFrame({"a": [1, 2, 3], "b": [2.0, 2.0, 2.0]})
     tm.assert_frame_equal(result, expected)
+
+
+[USER_REDACTED].mark.parametrize(
+    ("expr", "expected_values", "expected_str"),
+    [
+        (
+            pd.col("a") & pd.col("b"),
+            [False, False, True, False],
+            "(col('a') & col('b'))",
+        ),
+        (
+            pd.col("a") & True,
+            [True, False, True, False],
+            "(col('a') & True)",
+        ),
+        (
+            pd.col("a") | pd.col("b"),
+            [True, True, True, True],
+            "(col('a') | col('b'))",
+        ),
+        (
+            pd.col("a") | False,
+            [True, False, True, False],
+            "(col('a') | False)",
+        ),
+        (
+            pd.col("a") ^ pd.col("b"),
+            [True, True, False, True],
+            "(col('a') ^ col('b'))",
+        ),
+        (
+            pd.col("a") ^ True,
+            [False, True, False, True],
+            "(col('a') ^ True)",
+        ),
+        (
+            ~pd.col("a"),
+            [False, True, False, True],
+            "(~col('a'))",
+        ),
+    ],
+)
+def test_col_logical_ops(
+    expr: Expression, expected_values: list[bool], expected_str: str
+) -> None:
+    # https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63322
+    df = pd.DataFrame({"a": [True, False, True, False], "b": [False, True, True, True]})
+    result = df.assign(c=expr)
+    expected = pd.DataFrame(
+        {
+            "a": [True, False, True, False],
+            "b": [False, True, True, True],
+            "c": expected_values,
+        }
+    )
+    tm.assert_frame_equal(result, expected)
+    assert str(expr) == expected_str
+
+    # Test that the expression works with .loc
+    result = df.loc[expr]
+    expected = df[expected_values]
+    tm.assert_frame_equal(result, expected)

Thanks for the PR! Can you add tests.

Looking good!

Ah, I missed one request.

lgtm

Can you remove the comments throughout the parametrization.

`list[bool]`, yea?

Instead of this, can you test loc in the test above? I believe it'd just be:

```python
result = df.loc[expr]
expected = df[expected_values]
tm.assert_frame_equal(result, expected)
```

```suggestion
) -> None:
    # https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63322
    df = pd.DataFrame({"a": [True, False, True, False], "b": [False, True, True, True]})
```


BUG: Fix inconsistent datetime types between calamine and openpyxl readers (#59186)

closes #59186

When reading Excel files with mixed data types (strings, dates, numbers in the same column), the calamine and openpyxl engines returned different Python types for datetime values:

| Engine | Returned type |
|--------|---------------|
| openpyxl | `datetime.datetime` |
| calamine | `pd.Timestamp` |

This is confusing for users who expect consistent behavior regardless of which engine they use.

This PR Changes the calamine reader to return standard library types (`datetime.datetime`, `datetime.timedelta`) instead of pandas types (`pd.Timestamp`, `pd.Timedelta`), matching openpyxl's behavior.

Testing:
All existing Excel reader tests pass (1467 passed)
Manual testing confirms both engines now return the same types

@@ -15,7 +15,6 @@
 from pandas.compat._optional import import_optional_dependency
 from pandas.util._decorators import doc

-import pandas as pd
 from pandas.core.shared_docs import _shared_docs

 from pandas.io.excel._base import BaseExcelReader
@@ -107,10 +106,12 @@ def _convert_cell(value: _CellValue) -> Scalar | NaTType | time:
                     return val
                 else:
                     return value
+            elif isinstance(value, (datetime, timedelta)):
+                # Return as-is to match openpyxl behavior (GH#59186)
+                return value
             elif isinstance(value, date):
-                return pd.Timestamp(value)
-            elif isinstance(value, timedelta):
-                return pd.Timedelta(value)
+                # Convert date to datetime to match openpyxl behavior (GH#59186)
+                return datetime(value.year, value.month, value.day)
             elif isinstance(value, time):
                 return value

@@ -135,12 +135,7 @@ def df_ref(datapath):

 def get_exp_unit(read_ext: str, engine: str | None) -> str:
-    unit = "us"
-    if read_ext == ".ods" and engine == "odf":
-        pass
-    elif (read_ext == ".ods") ^ (engine == "calamine"):
-        unit = "s"
-    return unit
+    return "us"

 def adjust_expected(expected: DataFrame, read_ext: str, engine: str | None) -> None:

Can you combine this branch with the `isinstance(value, datetime)` branch by [NAME_REDACTED]?

Done, thanks!


inline docstrings for read_excel and storage options decorators in pa…

…ndas/io/excel/_base.py.

- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

@@ -11,7 +11,6 @@
 from decimal import Decimal
 from functools import partial
 import os
-from textwrap import fill
 from typing import (
     IO,
     TYPE_CHECKING,
@@ -30,15 +29,12 @@
 from pandas._config import config

 from pandas._libs import lib
-from pandas._libs.parsers import STR_NA_VALUES
 from pandas.compat._optional import (
     get_version,
     import_optional_dependency,
 )
 from pandas.errors import EmptyDataError
 from pandas.util._decorators import (
-    Appender,
-    doc,
     set_module,
 )
 from pandas.util._exceptions import find_stack_level
@@ -54,7 +50,6 @@
 )

 from pandas.core.frame import DataFrame
-from pandas.core.shared_docs import _shared_docs
 from pandas.util.version import Version

 from pandas.io.common import (
@@ -88,274 +83,6 @@
         StorageOptions,
         WriteExcelBuffer,
     )
-_read_excel_doc = (
-    """
-Read an Excel file into a ``DataFrame``.
-
-Supports `xls`, `xlsx`, `xlsm`, `xlsb`, `odf`, `ods` and `odt` file extensions
-read from a local filesystem or URL. Supports an option to read
-a single sheet or a list of sheets.
-
-Parameters
-----------
-io : str, ExcelFile, xlrd.Book, path object, or file-like object
-    Any valid string path is acceptable. The string could be a URL. Valid
-    URL schemes include http, ftp, s3, and file. For file URLs, a host is
-    expected. A local file could be: ``file://localhost/path/to/table.xlsx``.
-
-    If you want to pass in a path object, pandas accepts any ``os.PathLike``.
-
-    By [NAME_REDACTED], we refer to objects with a ``read()`` method,
-    such as a file handle (e.g. via builtin ``open`` function)
-    or ``StringIO``.
-
-    .. deprecated:: 2.1.0
-        Passing byte strings is deprecated. To read from a
-        byte string, wrap it in a ``BytesIO`` object.
-sheet_name : str, int, list, or None, default 0
-    Strings are used for sheet names. Integers are used in zero-indexed
-    sheet positions (chart sheets do not count as a sheet position).
-    Lists of strings/integers are used to request multiple sheets.
-    When ``None``, will return a dictionary containing DataFrames for each sheet.
-
-    Available cases:
-
-    * Defaults to ``0``: 1st sheet as a `DataFrame`
-    * ``1``: 2nd sheet as a `DataFrame`
-    * ``"Sheet1"``: Load sheet with name "Sheet1"
-    * ``[0, 1, "Sheet5"]``: Load first, second and sheet named "Sheet5"
-      as a dict of `DataFrame`
-    * ``None``: Returns a dictionary containing DataFrames for each sheet..
-
-header : int, list of int, default 0
-    Row (0-indexed) to use for the column labels of the parsed
-    DataFrame. If a list of integers is passed those row positions will
-    be combined into a ``MultiIndex``. Use None if there is no header.
-names : array-like, default None
-    List of column names to use. If file contains no header row,
-    then you should explicitly pass header=None.
-index_col : int, str, list of int, default None
-    Column (0-indexed) to use as the row labels of the DataFrame.
-    [NAME_REDACTED] if there is no such column.  If a list is passed,
-    those columns will be combined into a ``MultiIndex``.  If a
-    subset of data is selected with ``usecols``, index_col
-    is based on the subset.
-
-    Missing values will be forward filled to allow roundtripping with
-    ``to_excel`` for ``merged_cells=True``. To avoid forward filling the
-    missing values use ``set_index`` after reading the data instead of
-    ``index_col``.
-usecols : str, list-like, or callable, default None
-    * If None, then parse all columns.
-    * If str, then indicates comma separated list of Excel column letters
-      and column ranges (e.g. "A:E" or "A,C,E:F"). Ranges are inclusive of
-      both sides.
-    * If list of int, then indicates list of column numbers to be parsed
-      (0-indexed).
-    * If list of string, then indicates list of column names to be parsed.
-    * If callable, then evaluate each column name against it and parse the
-      column if the callable returns ``True``.
-
-    Returns a subset of the columns according to behavior above.
-dtype : Type name or dict of column -> type, default None
-    Data type for data or columns. E.g. {{'a': np.float64, 'b': np.int32}}
-    Use ``object`` to preserve data as stored in Excel and not interpret dtype,
-    which will necessarily result in ``object`` dtype.
-    If converters are specified, they will be applied INSTEAD
-    of dtype conversion.
-    If you use ``None``, it will infer the dtype of each column based on the data.
-engine : {{'openpyxl', 'calamine', 'odf', 'pyxlsb', 'xlrd'}}, default None
-    If io is not a buffer or path, this must be set to identify io.
-    Engine compatibility :
-
-    - ``openpyxl`` supports newer Excel file formats.
-    - ``calamine`` supports Excel (.xls, .xlsx, .xlsm, .xlsb)
-      and OpenDocument (.ods) file formats.
-    - ``odf`` supports OpenDocument file formats (.odf, .ods, .odt).
-    - ``pyxlsb`` supports [NAME_REDACTED] files.
-    - ``xlrd`` supports old-style Excel files (.xls).
-
-    When ``engine=None``, the following logic will be used to determine the engine:
-
-    - If ``path_or_buffer`` is an OpenDocument format (.odf, .ods, .odt),
-      then `odf <https://pypi.org/project/odfpy/>`_ will be used.
-    - Otherwise if ``path_or_buffer`` is an xls format, ``xlrd`` will be used.
-    - Otherwise if ``path_or_buffer`` is in xlsb format, ``pyxlsb`` will be used.
-    - Otherwise ``openpyxl`` will be used.
-converters : dict, default None
-    Dict of functions for converting values in certain columns. Keys can
-    either be integers or column labels, values are functions that take one
-    input argument, the Excel cell content, and return the transformed
-    content.
-true_values : list, default None
-    Values to consider as True.
-false_values : list, default None
-    Values to consider as False.
-skiprows : list-like, int, or callable, optional
-    Line numbers to skip (0-indexed) or number of lines to skip (int) at the
-    start of the file. If callable, the callable function will be evaluated
-    against the row indices, returning True if the row should be skipped and
-    False otherwise. An example of a valid callable argument would be ``lambda
-    x: x in [0, 2]``.
-nrows : int, default None
-    Number of rows to parse. Does not include header rows.
-na_values : scalar, str, list-like, or dict, default None
-    Additional strings to recognize as NA/NaN. If dict passed, specific
-    per-column NA values. By [NAME_REDACTED] are interpreted
-    as NaN: '"""
-    + fill("', '".join(sorted(STR_NA_VALUES)), 70, subsequent_indent="    ")
-    + """'.
-keep_default_na : bool, default True
-    Whether or not to include the default NaN values when parsing the data.
-    Depending on whether ``na_values`` is passed in, the behavior is as follows:
-
-    * If ``keep_default_na`` is True, and ``na_values`` are specified,
-      ``na_values`` is appended to the default NaN values used for parsing.
-    * If ``keep_default_na`` is True, and ``na_values`` are not specified, only
-      the default NaN values are used for parsing.
-    * If ``keep_default_na`` is False, and ``na_values`` are specified, only
-      the NaN values specified ``na_values`` are used for parsing.
-    * If ``keep_default_na`` is False, and ``na_values`` are not specified, no
-      strings will be parsed as NaN.
-
-    Note that if `na_filter` is passed in as False, the ``keep_default_na`` and
-    ``na_values`` parameters will be ignored.
-na_filter : bool, default True
-    Detect missing value markers (empty strings and the value of na_values). In
-    data without any NAs, passing ``na_filter=False`` can improve the
-    performance of reading a large file.
-verbose : bool, default False
-    Indicate number of NA values placed in non-numeric columns.
-parse_dates : bool, list-like, or dict, default False
-    The behavior is as follows:
-
-    * ``bool``. If True -> try parsing the index.
-    * ``list`` of int or names. e.g. If [1, 2, 3] -> try parsing columns 1, 2, 3
-      each as a separate date column.
-    * ``list`` of lists. e.g.  If [[1, 3]] -> combine columns 1 and 3 and parse as
-      a single date column.
-    * ``dict``, e.g. {{'foo' : [1, 3]}} -> parse columns 1, 3 as date and call
-      result 'foo'
-
-    If a column or index contains an unparsable date, the entire column or
-    index will be returned unaltered as an object data type. If you don`t want to
-    parse some cells as date just change their type in Excel to "Text".
-    For non-standard datetime parsing, use ``pd.to_datetime`` after ``pd.read_excel``.
-
-    Note: A fast-path exists for iso8601-formatted dates.
-date_format : str or dict of column -> format, default ``None``
-   If used in conjunction with ``parse_dates``, will parse dates according to this
-   format. For anything more complex,
-   please read in as ``object`` and then apply :func:`to_datetime` as-needed.
-
-   .. versionadded:: 2.0.0
-thousands : str, default None
-    Thousands separator for parsing string columns to numeric.  Note that
-    this parameter is only necessary for columns stored as TEXT in Excel,
-    any numeric columns will automatically be parsed, regardless of display
-    format.
-decimal : str, default '.'
-    Character to recognize as decimal point for parsing string columns to numeric.
-    Note that this parameter is only necessary for columns stored as TEXT in Excel,
-    any numeric columns will automatically be parsed, regardless of display
-    format.(e.g. use ',' for European data).
-comment : str, default None
-    Comments out remainder of line. Pass a character or characters to this
-    argument to indicate comments in the input file. Any data between the
-    comment string and the end of the current line is ignored.
-skipfooter : int, default 0
-    Rows at the end to skip (0-indexed).
-{storage_options}
-
-dtype_backend : {{'numpy_nullable', 'pyarrow'}}
-    Back-end data type applied to the resultant :class:`DataFrame`
-    (still experimental). If not specified, the default behavior
-    is to not use nullable data types. If specified, the behavior
-    is as follows:
-
-    * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame`
-    * ``"pyarrow"``: returns pyarrow-backed nullable
-      :class:`ArrowDtype` :class:`DataFrame`
-
-    .. versionadded:: 2.0
-
-engine_kwargs : dict, optional
-    Arbitrary keyword arguments passed to excel engine.
-
-Returns
--------
-DataFrame or dict of DataFrames
-    DataFrame from the passed in Excel file. See notes in sheet_name
-    argument for more information on when a dict of DataFrames is returned.
-
-See Also
---------
-DataFrame.to_excel : [NAME_REDACTED] to an Excel file.
-DataFrame.to_csv : [NAME_REDACTED] to a comma-separated values (csv) file.
-read_csv : Read a comma-separated values (csv) file into DataFrame.
-read_fwf : Read a table of fixed-width formatted lines into DataFrame.
-
-Notes
------
-For specific information on the methods used for each Excel engine, refer to the pandas
-:ref:`user guide <io.excel_reader>`
-
-Examples
---------
-The file can be read using the file name as string or an open file object:
-
->>> pd.read_excel('tmp.xlsx', index_col=0)  # doctest: +SKIP
-       [NAME_REDACTED]
-0   string1      1
-1   string2      2
-2  #Comment      3
-
->>> pd.read_excel(open('tmp.xlsx', 'rb'),
-...               sheet_name='Sheet3')  # doctest: +SKIP
-   Unnamed: 0      [NAME_REDACTED]
-0           0   string1      1
-1           1   string2      2
-2           2  #Comment      3
-
-Index and header can be specified via the `index_col` and `header` arguments
-
->>> pd.read_excel('tmp.xlsx', index_col=None, header=None)  # doctest: +SKIP
-     0         1      2
-0  [NAME_REDACTED]
-1  0.0   string1      1
-2  1.0   string2      2
-3  2.0  #Comment      3
-
-Column types are inferred but can be explicitly specified
-
->>> pd.read_excel('tmp.xlsx', index_col=0,
-...               dtype={{'Name': str, 'Value': float}})  # doctest: +SKIP
-       [NAME_REDACTED]
-0   string1    1.0
-1   string2    2.0
-2  #Comment    3.0
-
-True, False, and NA values, and thousands separators have defaults,
-but can be explicitly specified, too. Supply the values you would like
-as strings or lists of strings!
-
->>> pd.read_excel('tmp.xlsx', index_col=0,
-...               na_values=['string1', 'string2'])  # doctest: +SKIP
-       [NAME_REDACTED]
-0       NaN      1
-1       NaN      2
-2  #Comment      3
-
-Comment lines in the excel input file can be skipped using the
-``comment`` kwarg.
-
->>> pd.read_excel('tmp.xlsx', index_col=0, comment='#')  # doctest: +SKIP
-      [NAME_REDACTED]
-0  string1    1.0
-1  string2    2.0
-2     None    NaN
-"""
-)

 [USER_REDACTED]
@@ -433,8 +160,6 @@ def read_excel(

 [USER_REDACTED]_module("pandas")
-[USER_REDACTED](storage_options=_shared_docs["storage_options"])
-[USER_REDACTED](_read_excel_doc)
 def read_excel(
     io,
     sheet_name: str | int | list[IntStrT] | None = 0,
@@ -469,6 +194,286 @@ def read_excel(
     dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default,
     engine_kwargs: dict | None = None,
 ) -> DataFrame | dict[IntStrT, DataFrame]:
+    """
+    Read an Excel file into a ``DataFrame``.
+
+    Supports `xls`, `xlsx`, `xlsm`, `xlsb`, `odf`, `ods` and `odt` file extensions
+    read from a local filesystem or URL. Supports an option to read
+    a single sheet or a list of sheets.
+
+    Parameters
+    ----------
+    io : str, ExcelFile, xlrd.Book, path object, or file-like object
+        Any valid string path is acceptable. The string could be a URL. Valid
+        URL schemes include http, ftp, s3, and file. For file URLs, a host is
+        expected. A local file could be: ``file://localhost/path/to/table.xlsx``.
+
+        If you want to pass in a path object, pandas accepts any ``os.PathLike``.
+
+        By [NAME_REDACTED], we refer to objects with a ``read()`` method,
+        such as a file handle (e.g. via builtin ``open`` function)
+        or ``StringIO``.
+
+        .. deprecated:: 2.1.0
+            Passing byte strings is deprecated. To read from a
+            byte string, wrap it in a ``BytesIO`` object.
+    sheet_name : str, int, list, or None, default 0
+        Strings are used for sheet names. Integers are used in zero-indexed
+        sheet positions (chart sheets do not count as a sheet position).
+        Lists of strings/integers are used to request multiple sheets.
+        When ``None``, will return a dictionary containing DataFrames for each sheet.
+
+        Available cases:
+
+        * Defaults to ``0``: 1st sheet as a `DataFrame`
+        * ``1``: 2nd sheet as a `DataFrame`
+        * ``"Sheet1"``: Load sheet with name "Sheet1"
+        * ``[0, 1, "Sheet5"]``: Load first, second and sheet named "Sheet5"
+          as a dict of `DataFrame`
+        * ``None``: Returns a dictionary containing DataFrames for each sheet.
+
+    header : int, list of int, default 0
+        Row (0-indexed) to use for the column labels of the parsed
+        DataFrame. If a list of integers is passed those row positions will
+        be combined into a ``MultiIndex``. Use None if there is no header.
+    names : array-like, default None
+        List of column names to use. If file contains no header row,
+        then you should explicitly pass header=None.
+    index_col : int, str, list of int, default None
+        Column (0-indexed) to use as the row labels of the DataFrame.
+        [NAME_REDACTED] if there is no such column.  If a list is passed,
+        those columns will be combined into a ``MultiIndex``.  If a
+        subset of data is selected with ``usecols``, index_col
+        is based on the subset.
+
+        Missing values will be forward filled to allow roundtripping with
+        ``to_excel`` for ``merged_cells=True``. To avoid forward filling the
+        missing values use ``set_index`` after reading the data instead of
+        ``index_col``.
+    usecols : str, list-like, or callable, default None
+        * If None, then parse all columns.
+        * If str, then indicates comma separated list of Excel column letters
+          and column ranges (e.g. "A:E" or "A,C,E:F"). Ranges are inclusive of
+          both sides.
+        * If list of int, then indicates list of column numbers to be parsed
+          (0-indexed).
+        * If list of string, then indicates list of column names to be parsed.
+        * If callable, then evaluate each column name against it and parse the
+          column if the callable returns ``True``.
+
+        Returns a subset of the columns according to behavior above.
+    dtype : Type name or dict of column -> type, default None
+        Data type for data or columns. E.g. {'a': np.float64, 'b': np.int32}
+        Use ``object`` to preserve data as stored in Excel and not interpret dtype,
+        which will necessarily result in ``object`` dtype.
+        If converters are specified, they will be applied INSTEAD
+        of dtype conversion.
+        If you use ``None``, it will infer the dtype of each column based on the data.
+    engine : {'openpyxl', 'calamine', 'odf', 'pyxlsb', 'xlrd'}, default None
+        If io is not a buffer or path, this must be set to identify io.
+        Engine compatibility :
+
+        - ``openpyxl`` supports newer Excel file formats.
+        - ``calamine`` supports Excel (.xls, .xlsx, .xlsm, .xlsb)
+          and OpenDocument (.ods) file formats.
+        - ``odf`` supports OpenDocument file formats (.odf, .ods, .odt).
+        - ``pyxlsb`` supports [NAME_REDACTED] files.
+        - ``xlrd`` supports old-style Excel files (.xls).
+
+        When ``engine=None``, the following logic will be used to determine the engine:
+
+        - If ``path_or_buffer`` is an OpenDocument format (.odf, .ods, .odt),
+          then `odf <https://pypi.org/project/odfpy/>`_ will be used.
+        - Otherwise if ``path_or_buffer`` is an xls format, ``xlrd`` will be used.
+        - Otherwise if ``path_or_buffer`` is in xlsb format, ``pyxlsb`` will be used.
+        - Otherwise ``openpyxl`` will be used.
+
+    converters : dict, default None
+        Dict of functions for converting values in certain columns. Keys can
+        either be integers or column labels, values are functions that take one
+        input argument, the Excel cell content, and return the transformed
+        content.
+    true_values : list, default None
+        Values to consider as True.
+    false_values : list, default None
+        Values to consider as False.
+    skiprows : list-like, int, or callable, optional
+        Line numbers to skip (0-indexed) or number of lines to skip (int) at the
+        start of the file. If callable, the callable function will be evaluated
+        against the row indices, returning True if the row should be skipped and
+        False otherwise. An example of a valid callable argument would be ``lambda
+        x: x in [0, 2]``.
+    nrows : int, default None
+        Number of rows to parse. Does not include header rows.
+    na_values : scalar, str, list-like, or dict, default None
+        Additional strings to recognize as NA/NaN. If dict passed, specific
+        per-column NA values. By [NAME_REDACTED] are interpreted
+        as NaN: '', '#N/A', '#N/A N/A', '#NA', '-1.#IND', '-1.#QNAN', '-NaN', '-nan',
+        '1.#IND', '1.#QNAN', '<NA>', 'N/A', 'NA', 'NULL', 'NaN', 'None',
+        'n/a', 'nan', 'null'.
+    keep_default_na : bool, default True
+        Whether or not to include the default NaN values when parsing the data.
+        Depending on whether ``na_values`` is passed in, the behavior is as follows:
+
+        * If ``keep_default_na`` is True, and ``na_values`` are specified,
+          ``na_values`` is appended to the default NaN values used for parsing.
+        * If ``keep_default_na`` is True, and ``na_values`` are not specified, only
+          the default NaN values are used for parsing.
+        * If ``keep_default_na`` is False, and ``na_values`` are specified, only
+          the NaN values specified ``na_values`` are used for parsing.
+        * If ``keep_default_na`` is False, and ``na_values`` are not specified, no
+          strings will be parsed as NaN.
+
+        Note that if `na_filter` is passed in as False, the ``keep_default_na`` and
+        ``na_values`` parameters will be ignored.
+    na_filter : bool, default True
+        Detect missing value markers (empty strings and the value of na_values). In
+        data without any NAs, passing ``na_filter=False`` can improve the
+        performance of reading a large file.
+    verbose : bool, default False
+        Indicate number of NA values placed in non-numeric columns.
+    parse_dates : bool, list-like, or dict, default False
+        The behavior is as follows:
+
+        * ``bool``. If True -> try parsing the index.
+        * ``list`` of int or names. e.g. If [1, 2, 3] -> try parsing columns 1, 2, 3
+          each as a separate date column.
+        * ``list`` of lists. e.g.  If [[1, 3]] -> combine columns 1 and 3 and parse as
+          a single date column.
+        * ``dict``, e.g. {'foo' : [1, 3]} -> parse columns 1, 3 as date and call
+          result 'foo'
+
+        If a column or index contains an unparsable date, the entire column or
+        index will be returned unaltered as an object data type. If you don`t want to
+        parse some cells as date just change their type in Excel to "Text".
+        For non-standard datetime parsing, use ``pd.to_datetime`` after
+        ``pd.read_excel``.
+
+        Note: A fast-path exists for iso8601-formatted dates.
+    date_format : str or dict of column -> format, default ``None``
+        If used in conjunction with ``parse_dates``, will parse dates according to this
+        format. For anything more complex,
+        please read in as ``object`` and then apply :func:`to_datetime` as-needed.
+
+        .. versionadded:: 2.0.0
+
+    thousands : str, default None
+        Thousands separator for parsing string columns to numeric.  Note that
+        this parameter is only necessary for columns stored as TEXT in Excel,
+        any numeric columns will automatically be parsed, regardless of display
+        format.
+    decimal : str, default '.'
+        Character to recognize as decimal point for parsing string columns to numeric.
+        Note that this parameter is only necessary for columns stored as TEXT in Excel,
+        any numeric columns will automatically be parsed, regardless of display
+        format.(e.g. use ',' for European data).
+    comment : str, default None
+        Comments out remainder of line. Pass a character or characters to this
+        argument to indicate comments in the input file. Any data between the
+        comment string and the end of the current line is ignored.
+    skipfooter : int, default 0
+        Rows at the end to skip (0-indexed).
+    storage_options : dict, optional
+        Extra options that make sense for a particular storage connection, e.g.
+        host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
+        are forwarded to ``urllib.request.Request`` as header options. For other
+        URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
+        forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
+        details, and for more examples on storage options refer `here
+        <https://pandas.pydata.org/docs/user_guide/io.html?
+        highlight=storage_options#reading-writing-remote-files>`_.
+
+    dtype_backend : {'numpy_nullable', 'pyarrow'}
+        Back-end data type applied to the resultant :class:`DataFrame`
+        (still experimental). If not specified, the default behavior
+        is to not use nullable data types. If specified, the behavior
+        is as follows:
+
+        * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame`
+        * ``"pyarrow"``: returns pyarrow-backed nullable
+
+        :class:`ArrowDtype` :class:`DataFrame`
+
+        .. versionadded:: 2.0
+
+    engine_kwargs : dict, optional
+        Arbitrary keyword arguments passed to excel engine.
+
+    Returns
+    -------
+    DataFrame or dict of DataFrames
+        DataFrame from the passed in Excel file. See notes in sheet_name
+        argument for more information on when a dict of DataFrames is returned.
+
+    See Also
+    --------
+    DataFrame.to_excel : [NAME_REDACTED] to an Excel file.
+    DataFrame.to_csv : [NAME_REDACTED] to a comma-separated values (csv) file.
+    read_csv : Read a comma-separated values (csv) file into DataFrame.
+    read_fwf : Read a table of fixed-width formatted lines into DataFrame.
+
+    Notes
+    -----
+    For specific information on the methods used for each Excel engine, refer to the
+    pandas
+    :ref:`user guide <io.excel_reader>`
+
+    Examples
+    --------
+    The file can be read using the file name as string or an open file object:
+
+    >>> pd.read_excel("tmp.xlsx", index_col=0)  # doctest: +SKIP
+           [NAME_REDACTED]
+    0   string1      1
+    1   string2      2
+    2  #Comment      3
+
+    >>> pd.read_excel(open("tmp.xlsx", "rb"), sheet_name="Sheet3")  # doctest: +SKIP
+       Unnamed: 0      [NAME_REDACTED]
+    0           0   string1      1
+    1           1   string2      2
+    2           2  #Comment      3
+
+    Index and header can be specified via the `index_col` and `header` arguments
+
+    >>> pd.read_excel("tmp.xlsx", index_col=None, header=None)  # doctest: +SKIP
+         0         1      2
+    0  [NAME_REDACTED]
+    1  0.0   string1      1
+    2  1.0   string2      2
+    3  2.0  #Comment      3
+
+    Column types are inferred but can be explicitly specified
+
+    >>> pd.read_excel(
+    ...     "tmp.xlsx", index_col=0, dtype={"Name": str, "Value": float}
+    ... )  # doctest: +SKIP
+           [NAME_REDACTED]
+    0   string1    1.0
+    1   string2    2.0
+    2  #Comment    3.0
+
+    True, False, and NA values, and thousands separators have defaults,
+    but can be explicitly specified, too. Supply the values you would like
+    as strings or lists of strings!
+
+    >>> pd.read_excel(
+    ...     "tmp.xlsx", index_col=0, na_values=["string1", "string2"]
+    ... )  # doctest: +SKIP
+           [NAME_REDACTED]
+    0       NaN      1
+    1       NaN      2
+    2  #Comment      3
+
+    Comment lines in the excel input file can be skipped using the
+    ``comment`` kwarg.
+
+    >>> pd.read_excel("tmp.xlsx", index_col=0, comment="#")  # doctest: +SKIP
+          [NAME_REDACTED]
+    0  string1    1.0
+    1  string2    2.0
+    2     None    NaN
+    """
     check_dtype_backend(dtype_backend)
     should_close = False
     if engine_kwargs is None:
@@ -951,7 +956,6 @@ def _parse_sheet(

 [USER_REDACTED]_module("pandas")
-[USER_REDACTED](storage_options=_shared_docs["storage_options"])
 class ExcelWriter(Generic[_WorkbookT]):
     """
     Class for writing DataFrame objects into excel sheets.
@@ -982,7 +986,15 @@ class ExcelWriter(Generic[_WorkbookT]):
         (e.g. 'YYYY-MM-DD HH:MM:SS').
     mode : {{'w', 'a'}}, default 'w'
         File mode to use (write or append). Append does not work with fsspec URLs.
-    {storage_options}
+    storage_options : dict, optional
+        Extra options that make sense for a particular storage connection, e.g.
+        host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
+        are forwarded to ``urllib.request.Request`` as header options. For other
+        URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
+        forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
+        details, and for more examples on storage options refer `here
+        <https://pandas.pydata.org/docs/user_guide/io.html?
+        highlight=storage_options#reading-writing-remote-files>`_.

     if_sheet_exists : {{'error', 'new', 'replace', 'overlay'}}, default 'error'
         How to behave when trying to write to a sheet that already
@@ -1405,7 +1417,6 @@ def close(self) -> None:
 PEEK_SIZE = max(map(len, XLS_SIGNATURES + (ZIP_SIGNATURE,)))

-[USER_REDACTED](storage_options=_shared_docs["storage_options"])
 def inspect_excel_format(
     content_or_path: FilePath | ReadBuffer[bytes],
     storage_options: StorageOptions | None = None,
@@ -1419,7 +1430,15 @@ def inspect_excel_format(
     ----------
     content_or_path : str or file-like object
         Path to file or content of file to inspect. May be a URL.
-    {storage_options}
+    storage_options : dict, optional
+        Extra options that make sense for a particular storage connection, e.g.
+        host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
+        are forwarded to ``urllib.request.Request`` as header options. For other
+        URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
+        forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
+        details, and for more examples on storage options refer `here
+        <https://pandas.pydata.org/docs/user_guide/io.html?
+        highlight=storage_options#reading-writing-remote-files>`_.

     Returns
     -------
@@ -1467,7 +1486,6 @@ def inspect_excel_format(

 [USER_REDACTED]_module("pandas")
-[USER_REDACTED](storage_options=_shared_docs["storage_options"])
 class ExcelFile:
     """
     Class for parsing tabular Excel sheets into DataFrame objects.
@@ -1511,7 +1529,15 @@ class ExcelFile:

            Please do not report issues when using ``xlrd`` to read ``.xlsx`` files.
            This is not supported, switch to using ``openpyxl`` instead.
-    {storage_options}
+    storage_options : dict, optional
+        Extra options that make sense for a particular storage connection, e.g.
+        host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
+        are forwarded to ``urllib.request.Request`` as header options. For other
+        URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
+        forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
+        details, and for more examples on storage options refer `here
+        <https://pandas.pydata.org/docs/user_guide/io.html?
+        highlight=storage_options#reading-writing-remote-files>`_.
     engine_kwargs : dict, optional
         Arbitrary keyword arguments passed to excel engine.

Can you make sure these column names are aligned to the values of the columns (similar below)?

I have updated the formatting as requested.


API: to_timedelta(ints, unit) give requested unit

- [x] part of #63275
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

Sits on top of #63302.  We still need to do the same for pd.to_datetime.

@@ -44,6 +44,7 @@ from pandas._libs.tslibs.conversion cimport (
     cast_from_unit,
 )
 from pandas._libs.tslibs.dtypes cimport (
+    abbrev_to_npy_unit,
     c_DEPR_UNITS,
     get_supported_reso,
     is_supported_unit,
@@ -359,11 +360,16 @@ def array_to_timedelta64(
         cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
         cnp.flatiter it
         str parsed_unit = parse_timedelta_unit(unit or "ns")
-        NPY_DATETIMEUNIT item_reso
+        NPY_DATETIMEUNIT item_reso, int_reso
         ResoState state = ResoState(creso)
         bint infer_reso = creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC
         ndarray iresult = result.view("i8")

+    if unit is None:
+        int_reso = NPY_FR_ns
+    else:
+        int_reso = get_supported_reso(abbrev_to_npy_unit(parsed_unit))
+
     if values.descr.type_num != cnp.NPY_OBJECT:
         # raise here otherwise we segfault below
         raise TypeError("array_to_timedelta64 'values' must have object dtype")
@@ -472,7 +478,18 @@ def array_to_timedelta64(
                     creso = state.creso
                 ival = delta_to_nanoseconds(item, reso=creso)

-            elif is_integer_object(item) or is_float_object(item):
+            elif is_integer_object(item):
+                if item == NPY_NAT:
+                    ival = NPY_NAT
+                else:
+                    ival = _numeric_to_td64ns(item, parsed_unit, int_reso)
+                    item_reso = int_reso
+
+                    state.update_creso(item_reso)
+                    if infer_reso:
+                        creso = state.creso
+
+            elif is_float_object(item):
                 ival = _numeric_to_td64ns(item, parsed_unit, NPY_FR_ns)

                 item_reso = NPY_FR_ns

@@ -1656,7 +1656,7 @@ def mean(self, *, skipna: bool = True, axis: AxisInt | None = 0):
         >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
         >>> tdelta_idx
         TimedeltaIndex(['1 days', '2 days', '3 days'],
-                        dtype='timedelta64[ns]', freq=None)
+                        dtype='timedelta64[s]', freq=None)
         >>> tdelta_idx.mean()
         Timedelta('2 days 00:00:00')
         """

@@ -850,7 +850,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
         >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
         >>> tdelta_idx
         TimedeltaIndex(['1 days', '2 days', '3 days'],
-                        dtype='timedelta64[ns]', freq=None)
+                        dtype='timedelta64[s]', freq=None)
         >>> tdelta_idx.to_pytimedelta()
         array([datetime.timedelta(days=1), datetime.timedelta(days=2),
                datetime.timedelta(days=3)], dtype=object)
@@ -883,7 +883,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
     0   1 days
     1   2 days
     2   3 days
-    dtype: timedelta64[ns]
+    dtype: timedelta64[s]
     >>> ser.dt.days
     0    1
     1    2
@@ -918,7 +918,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
     0   0 days 00:00:01
     1   0 days 00:00:02
     2   0 days 00:00:03
-    dtype: timedelta64[ns]
+    dtype: timedelta64[s]
     >>> ser.dt.seconds
     0    1
     1    2
@@ -930,7 +930,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
     >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='s')
     >>> tdelta_idx
     TimedeltaIndex(['0 days 00:00:01', '0 days 00:00:02', '0 days 00:00:03'],
-                   dtype='timedelta64[ns]', freq=None)
+                   dtype='timedelta64[s]', freq=None)
     >>> tdelta_idx.seconds
     Index([1, 2, 3], dtype='int32')"""
     )
@@ -958,7 +958,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
     0   0 days 00:00:00.000001
     1   0 days 00:00:00.000002
     2   0 days 00:00:00.000003
-    dtype: timedelta64[ns]
+    dtype: timedelta64[us]
     >>> ser.dt.microseconds
     0    1
     1    2
@@ -971,7 +971,7 @@ def to_pytimedelta(self) -> npt.NDArray[np.object_]:
     >>> tdelta_idx
     TimedeltaIndex(['0 days 00:00:00.000001', '0 days 00:00:00.000002',
                     '0 days 00:00:00.000003'],
-                   dtype='timedelta64[ns]', freq=None)
+                   dtype='timedelta64[us]', freq=None)
     >>> tdelta_idx.microseconds
     Index([1, 2, 3], dtype='int32')"""
     )
@@ -1208,10 +1208,12 @@ def _ints_to_td64ns(data, unit: str = "ns") -> tuple[np.ndarray, bool]:
         dtype_str = f"timedelta64[{unit}]"
         data = data.view(dtype_str)

-        data = astype_overflowsafe(data, dtype=TD64NS_DTYPE)
+        new_dtype = get_supported_dtype(data.dtype)
+        if new_dtype != data.dtype:
+            data = astype_overflowsafe(data, dtype=new_dtype)

-        # the astype conversion makes a copy, so we can avoid re-copying later
-        copy_made = True
+            # the astype conversion makes a copy, so we can avoid re-copying later
+            copy_made = True

     else:
         data = data.view("timedelta64[ns]")

@@ -489,7 +489,7 @@ def to_pytimedelta(self) -> np.ndarray:
         2   2 days
         3   3 days
         4   4 days
-        dtype: timedelta64[ns]
+        dtype: timedelta64[s]

         >>> s.dt.to_pytimedelta()
         array([datetime.timedelta(0), datetime.timedelta(days=1),
@@ -535,7 +535,7 @@ def components(self) -> DataFrame:
         2   0 days 00:00:02
         3   0 days 00:00:03
         4   0 days 00:00:04
-        dtype: timedelta64[ns]
+        dtype: timedelta64[s]
         >>> s.dt.components
            days  hours  minutes  seconds  milliseconds  microseconds  nanoseconds
         0     0      0        0        0             0             0            0

@@ -132,7 +132,7 @@ def mean(self, *, skipna: bool = True, axis: int | None = 0):
         >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
         >>> tdelta_idx
         TimedeltaIndex(['1 days', '2 days', '3 days'],
-                        dtype='timedelta64[ns]', freq=None)
+                        dtype='timedelta64[s]', freq=None)
         >>> tdelta_idx.mean()
         Timedelta('2 days 00:00:00')
         """

@@ -167,10 +167,10 @@ def to_timedelta(
     >>> pd.to_timedelta(np.arange(5), unit="s")
     TimedeltaIndex(['0 days 00:00:00', '0 days 00:00:01', '0 days 00:00:02',
                     '0 days 00:00:03', '0 days 00:00:04'],
-                   dtype='timedelta64[ns]', freq=None)
+                   dtype='timedelta64[s]', freq=None)
     >>> pd.to_timedelta(np.arange(5), unit="D")
     TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
-                   dtype='timedelta64[ns]', freq=None)
+                   dtype='timedelta64[s]', freq=None)
     """
     if unit is not None:
         unit = parse_timedelta_unit(unit)

@@ -735,11 +735,12 @@ def test_tdi_add_overflow(self):
             ts + pd.to_timedelta(106580, "D")

         _NaT = NaT._value + 1
+        td = pd.to_timedelta([106580], "D").as_unit("ns")
         msg = "Overflow in int64 addition"
         with pytest.raises(OverflowError, match=msg):
-            pd.to_timedelta([106580], "D") + Timestamp("2000")
+            td + Timestamp("2000")
         with pytest.raises(OverflowError, match=msg):
-            Timestamp("2000") + pd.to_timedelta([106580], "D")
+            Timestamp("2000") + td
         with pytest.raises(OverflowError, match=msg):
             pd.to_timedelta([_NaT]) - Timedelta("1 days")
         with pytest.raises(OverflowError, match=msg):

@@ -363,15 +363,14 @@ def test_groupby_quantile_allNA_column(dtype):

 def test_groupby_timedelta_quantile():
     # GH: 29485
-    df = DataFrame(
-        {"value": pd.to_timedelta(np.arange(4), unit="s"), "group": [1, 1, 2, 2]}
-    )
+    tdi = pd.to_timedelta(np.arange(4), unit="s").as_unit("us")
+    df = DataFrame({"value": tdi, "group": [1, 1, 2, 2]})
     result = df.groupby("group").quantile(0.99)
     expected = DataFrame(
         {
             "value": [
-                pd.Timedelta("0 days 00:00:00.990000").as_unit("ns"),
-                pd.Timedelta("0 days 00:00:02.990000").as_unit("ns"),
+                pd.Timedelta("0 days 00:00:00.990000"),
+                pd.Timedelta("0 days 00:00:02.990000"),
             ]
         },
         index=Index([1, 2], name="group"),

@@ -49,7 +49,7 @@ def test_tdi_shift_int(self):
                 "5 days 01:00:00",
             ],
             freq="D",
-            dtype="m8[ns]",
+            dtype="m8[s]",
         )
         tm.assert_index_equal(result, expected)

@@ -67,7 +67,7 @@ def test_tdi_shift_nonstandard_freq(self):
                 "10 days 01:00:03",
             ],
             freq="D",
-            dtype="m8[ns]",
+            dtype="m8[s]",
         )
         tm.assert_index_equal(result, expected)

@@ -267,4 +267,5 @@ def test_unit_deprecated(self, unit, unit_depr):

         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             tdi = to_timedelta([1, 2], unit=unit_depr)
-        tm.assert_index_equal(tdi, expected.as_unit("ns"))
+        exp_unit = unit if unit in ["s", "ms", "us"] else "s"
+        tm.assert_index_equal(tdi, expected.as_unit(exp_unit))

@@ -1156,7 +1156,8 @@ def test_timedelta(self):

         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             json = frame.to_json()
-        tm.assert_frame_equal(frame, read_json(StringIO(json)).apply(converter))
+        result = read_json(StringIO(json)).apply(converter)
+        tm.assert_frame_equal(frame.astype("m8[ms]"), result)

     def test_timedelta2(self):
         frame = DataFrame(

@@ -101,9 +101,7 @@ def test_resample_categorical_data_with_timedeltaindex():
     df = DataFrame({"Group_obj": "A"}, index=pd.to_timedelta(list(range(20)), unit="s"))
     df["Group"] = df["Group_obj"].astype("category")
     result = df.resample("10s").agg(lambda x: (x.value_counts().index[0]))
-    exp_tdi = pd.TimedeltaIndex(np.array([0, 10], dtype="m8[s]"), freq="10s").as_unit(
-        "ns"
-    )
+    exp_tdi = pd.TimedeltaIndex(np.array([0, 10], dtype="m8[s]"), freq="10s")
     expected = DataFrame(
         {"Group_obj": ["A", "A"], "Group": ["A", "A"]},
         index=exp_tdi,

@@ -161,9 +161,10 @@ def test_unit_deprecated(self, unit, unit_depr):
     def test_unit_parser(self, unit, np_unit, wrapper):
         # validate all units, GH 6855, GH 21762
         # array-likes
+        exp_unit = np_unit if np_unit not in ["W", "D", "m"] else "s"
         expected = TimedeltaIndex(
             [np.timedelta64(i, np_unit) for i in np.arange(5).tolist()],
-            dtype="m8[ns]",
+            dtype=f"m8[{exp_unit}]",
         )

         result = to_timedelta(wrapper(range(5)), unit=unit)

@@ -2116,7 +2116,7 @@ def test_dataframe_field_aliases_column_subset(self, df, cache, unit):
         result = to_datetime(df[list(unit.keys())].rename(columns=unit), cache=cache)
         expected = Series(
             [Timestamp("20150204 06:58:10"), Timestamp("20160305 07:59:11")],
-            dtype="M8[ns]",
+            dtype="M8[us]",
         )
         tm.assert_series_equal(result, expected)

@@ -104,7 +104,7 @@ def test_to_timedelta_units(self):
         result = TimedeltaIndex(
             [np.timedelta64(0, "ns"), np.timedelta64(10, "s").astype("m8[ns]")]
         )
-        expected = to_timedelta([0, 10], unit="s")
+        expected = to_timedelta([0, 10], unit="s").as_unit("ns")
         tm.assert_index_equal(result, expected)

     [USER_REDACTED].mark.parametrize(
@@ -121,7 +121,7 @@ def test_to_timedelta_units_dtypes(self, dtype, unit):
         # arrays of various dtypes
         arr = np.array([1] * 5, dtype=dtype)
         result = to_timedelta(arr, unit=unit)
-        exp_dtype = "m8[ns]" if dtype == "int64" else "m8[s]"
+        exp_dtype = "m8[s]"
         expected = TimedeltaIndex([np.timedelta64(1, unit)] * 5, dtype=exp_dtype)
         tm.assert_index_equal(result, expected)

@@ -277,7 +277,7 @@ def test_to_timedelta_coerce_strings_unit(self):
     )
     def test_to_timedelta_nullable_int64_dtype(self, expected_val, result_val):
         # GH 35574
-        expected = Series([timedelta(days=1), expected_val], dtype="m8[ns]")
+        expected = Series([timedelta(days=1), expected_val], dtype="m8[s]")
         result = to_timedelta(Series([1, result_val], dtype="Int64"), unit="days")

         tm.assert_series_equal(result, expected)

Looks good!

Should this also be intended?

The to_datetime behaviour also changed because of this PR?

Yes.  to_datetime on a DataFrame calls to_timedelta.

good call, updated


API: pd.Timedelta(integer, unit=unit) give the requested unit

Matching the Timestamp behavior.  Part of #63275.  Sits on top of #63301.

@@ -409,6 +409,8 @@ In cases with mixed-resolution inputs, the highest resolution is used:

 Similarly, the :class:`Timedelta` constructor and :func:`to_timedelta` with a string input now defaults to a microsecond unit, using nanosecond unit only in cases that actually have nanosecond precision.

+Moreover, passing an integer to the :class:`Timedelta` constructor or :func:`to_timedelta` along with a ``unit`` will now return an object with that unit when possible, or the closest-supported unit for non-supported units ("W", "D", "h", "m").
+
 .. _whatsnew_300.api_breaking.concat_datetime_sorting:

 :func:`concat` no longer ignores ``sort`` when all objects have a :class:`DatetimeIndex`

@@ -290,22 +290,24 @@ cpdef int64_t delta_to_nanoseconds(
         ) from err

-cdef _numeric_to_td64ns(object item, str unit):
+cdef int64_t _numeric_to_td64ns(
+    object item, str unit, NPY_DATETIMEUNIT out_reso=NPY_FR_ns
+):
     # caller is responsible for checking
     #  assert unit not in ["Y", "y", "M"]
     #  assert is_integer_object(item) or is_float_object(item)
     if is_integer_object(item) and item == NPY_NAT:
-        return np.timedelta64(NPY_NAT, "ns")
+        return NPY_NAT

     try:
-        item = cast_from_unit(item, unit)
+        ival = cast_from_unit(item, unit, out_reso)
     except OutOfBoundsDatetime as err:
+        abbrev = npy_unit_to_abbrev(out_reso)
         raise OutOfBoundsTimedelta(
-            f"Cannot cast {item} from {unit} to 'ns' without overflow."
+            f"Cannot cast {item} from {unit} to '{abbrev}' without overflow."
         ) from err

-    ts = np.timedelta64(item, "ns")
-    return ts
+    return ival

 # TODO: de-duplicate with DatetimeParseState
@@ -352,7 +354,7 @@ def array_to_timedelta64(
     cdef:
         Py_ssize_t i, n = values.size
         ndarray result = np.empty((<object>values).shape, dtype="m8[ns]")
-        object item, td64ns_obj
+        object item
         int64_t ival
         cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
         cnp.flatiter it
@@ -471,8 +473,7 @@ def array_to_timedelta64(
                 ival = delta_to_nanoseconds(item, reso=creso)

             elif is_integer_object(item) or is_float_object(item):
-                td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
-                ival = cnp.get_timedelta64_value(td64ns_obj)
+                ival = _numeric_to_td64ns(item, parsed_unit, NPY_FR_ns)

                 item_reso = NPY_FR_ns
                 state.update_creso(item_reso)
@@ -2230,7 +2231,18 @@ class Timedelta(_Timedelta):
         elif checknull_with_nat_and_na(value):
             return NaT

-        elif is_integer_object(value) or is_float_object(value):
+        elif is_integer_object(value):
+            # unit=None is de-facto 'ns'
+            if value != NPY_NAT:
+                unit = parse_timedelta_unit(unit)
+                if unit != "ns":
+                    # Return with the closest-to-supported unit by [NAME_REDACTED]
+                    #  the timedelta64 path
+                    td = np.timedelta64(value, unit)
+                    return cls(td)
+                value = _numeric_to_td64ns(value, unit)
+
+        elif is_float_object(value):
             # unit=None is de-facto 'ns'
             unit = parse_timedelta_unit(unit)
             value = _numeric_to_td64ns(value, unit)

@@ -728,10 +728,11 @@ def test_tdi_add_overflow(self):
         # See GH#14068
         # preliminary test scalar analogue of vectorized tests below
         # TODO: Make raised error message more informative and test
+        ts = Timestamp("2000").as_unit("ns")
         with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
-            pd.to_timedelta(106580, "D") + Timestamp("2000")
+            pd.to_timedelta(106580, "D") + ts
         with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
-            Timestamp("2000") + pd.to_timedelta(106580, "D")
+            ts + pd.to_timedelta(106580, "D")

         _NaT = NaT._value + 1
         msg = "Overflow in int64 addition"

@@ -127,14 +127,15 @@ def test_mask_where_dtype_timedelta():
     # https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/39548
     df = DataFrame([Timedelta(i, unit="D") for i in range(5)])

-    expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[ns]"))
+    expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[s]"))
     tm.assert_frame_equal(df.mask(df.notna()), expected)

     expected = DataFrame(
         [np.nan, np.nan, np.nan, Timedelta("3 day"), Timedelta("4 day")],
-        dtype="m8[ns]",
+        dtype="m8[s]",
     )
-    tm.assert_frame_equal(df.where(df > Timedelta(2, unit="D")), expected)
+    result = df.where(df > Timedelta(2, unit="D"))
+    tm.assert_frame_equal(result, expected)

 def test_mask_return_dtype():

@@ -1000,8 +1000,8 @@ def test_loc_expansion_with_timedelta_type(self):
             index=Index([0]),
             columns=(["a", "b", "c"]),
         )
-        expected["a"] = expected["a"].astype("m8[ns]")
-        expected["b"] = expected["b"].astype("m8[ns]")
+        expected["a"] = expected["a"].astype("m8[s]")
+        expected["b"] = expected["b"].astype("m8[s]")
         tm.assert_frame_equal(result, expected)

     def test_setitem_tuple_key_in_empty_frame(self):

@@ -893,8 +893,8 @@ def create_data(constructor):
         [
             (lambda x: np.timedelta64(x, "D"), "m8[s]"),
             (lambda x: timedelta(days=x), "m8[us]"),
-            (lambda x: Timedelta(x, "D"), "m8[ns]"),
-            (lambda x: Timedelta(x, "D").as_unit("s"), "m8[s]"),
+            (lambda x: Timedelta(x, "D"), "m8[s]"),
+            (lambda x: Timedelta(x, "D").as_unit("ms"), "m8[ms]"),
         ],
     )
     def test_constructor_dict_timedelta64_index(self, klass, exp_dtype):

@@ -1141,15 +1141,15 @@ def test_timedelta(self):
         )
         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
-        tm.assert_series_equal(result, ser)
+        tm.assert_series_equal(result, ser.astype("m8[ms]"))

         ser = Series(
             [timedelta(23), timedelta(seconds=5)], index=Index([0, 1]), dtype="m8[ns]"
         )
         assert ser.dtype == "timedelta64[ns]"
         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
-        tm.assert_series_equal(result, ser)
+        tm.assert_series_equal(result, ser.astype("m8[ms]"))

         frame = DataFrame([timedelta(23), timedelta(seconds=5)], dtype="m8[ns]")
         assert frame[0].dtype == "timedelta64[ns]"

@@ -119,7 +119,7 @@ def test_td_add_datetimelike_scalar(self, op):

     def test_td_add_timestamp_overflow(self):
         ts = Timestamp("1700-01-01").as_unit("ns")
-        msg = "Cannot cast 259987 from D to 'ns' without overflow."
+        msg = "Cannot cast 259987 days 00:00:00 to unit='ns' without overflow."
         with pytest.raises(OutOfBoundsTimedelta, match=msg):
             ts + Timedelta(13 * 19999, unit="D")

@@ -35,6 +35,23 @@ def test_noninteger_microseconds(self):

 class TestTimedeltaConstructorUnitKeyword:
+    def test_result_unit(self):
+        # For supported units, we get result.unit == unit
+        for unit in ["s", "ms", "us", "ns"]:
+            td = Timedelta(1, unit=unit)
+            assert td.unit == unit
+
+            td = to_timedelta(1, unit=unit)
+            assert td.unit == unit
+
+        # For non-supported units we get the closest-supported unit
+        for unit in ["W", "D", "h", "m"]:
+            td = Timedelta(1, unit=unit)
+            assert td.unit == "s"
+
+            td = to_timedelta(1, unit=unit)
+            assert td.unit == "s"
+
     [USER_REDACTED].mark.parametrize("unit", ["Y", "y", "M"])
     def test_unit_m_y_raises(self, unit):
         msg = "Units 'M', 'Y', and 'y' are no longer supported"
@@ -196,7 +213,8 @@ def test_construct_from_kwargs_overflow():

 def test_construct_with_weeks_unit_overflow():
     # GH#47268 don't silently wrap around
-    with pytest.raises(OutOfBoundsTimedelta, match="without overflow"):
+    msg = "1000000000000000000 weeks"
+    with pytest.raises(OutOfBoundsTimedelta, match=msg):
         Timedelta(1000000000000000000, unit="W")

     with pytest.raises(OutOfBoundsTimedelta, match="without overflow"):
@@ -284,7 +302,7 @@ def test_from_tick_reso():

 def test_construction():
     expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
-    assert Timedelta(10, unit="D")._value == expected
+    assert Timedelta(10, unit="D")._value == expected // 10**9
     assert Timedelta(10.0, unit="D")._value == expected
     assert Timedelta("10 days")._value == expected // 1000
     assert Timedelta(days=10)._value == expected // 1000
@@ -464,9 +482,9 @@ def test_overflow_on_construction():
         Timedelta(value)

     # xref GH#17637
-    msg = "Cannot cast 139993 from D to 'ns' without overflow"
-    with pytest.raises(OutOfBoundsTimedelta, match=msg):
-        Timedelta(7 * 19999, unit="D")
+    # used to overflows before we changed output unit to "s"
+    td = Timedelta(7 * 19999, unit="D")
+    assert td.unit == "s"

     # used to overflow before non-ns support
     td = Timedelta(timedelta(days=13 * 19999))

Some points to consider:
1. Confirm cast_from_unit accepts the new out_reso argument (or update it).
2. Replace magic numeric bounds with named constants (or compute from datetime.timedelta.max/min) and add a short comment.
3. Add tests that check very large integer Timedelta inputs for weeks/days/hours/minutes (overflow edges) and tests that NaT values stay preserved."

You can keep those if you make the timestamp(or timedelta) ns with adding a `.as_unit("ns")` ?

```suggestion
            (lambda x: Timedelta(x, "D").as_unit("ms"), "m8[ms]"),
```

Otherwise it is the same as the case above now. Or just remove the case alltogether

Not at all related to your change, but wondering: do you know why we apply this converter instead of checking the actual return value of `read_json`? Because applying to_timedelta on the result (and which output change now) has nothing to do with the reading of json .. (I assume the reading actually returns strings)

Do you have a similar explicit test for pd.to_timedelta ?

Ah, that's because `to_timedelta` is not yet updated in this PR. I assumed it was based on some changed tests, but that was only for the case of a scalar input to `to_timedelta`, not array-like input


BUG: pydatetime + Timedelta[non-nano] incorrect results

- [ ] xref #53643 (Replace xxxx with the GitHub issue number)
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

Yak-shaving towards #63275.  Implementing that directly breaks tests for pydatetime + pd.Timedelta, which this fixes.  This will address the large majority of cases in #53643, so _could_ close it, but users will still get silent incorrect results if they have extremely large Timedeltas.

@@ -1131,6 +1131,7 @@ Timedelta
 - Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`)
 - Bug in :class:`Timedelta` constructor failing to raise when passed an invalid keyword (:issue:`53801`)
 - Bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
+- Bug in adding or subtracting a :class:`Timedelta` object with non-nanosecond unit to a python ``datetime.datetime`` object giving incorrect results; this now works correctly for Timedeltas inside the ``datetime.timedelta`` implementation bounds (:issue:`53643`)
 - Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`)
 - Bug in multiplication operations with ``timedelta64`` dtype incorrectly raising when multiplying by [NAME_REDACTED] integer dtypes (:issue:`58054`)

@@ -1022,9 +1022,23 @@ cdef _timedelta_from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso):
     elif reso == NPY_DATETIMEUNIT.NPY_FR_us:
         td_base = _Timedelta.__new__(cls, microseconds=int(value))
     elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
-        td_base = _Timedelta.__new__(cls, milliseconds=0)
+        if -86_399_999_913_600_000 <= value < 86_400_000_000_000_000:
+            # i.e. we are in range for pytimedelta. By [NAME_REDACTED]
+            #  'correct' value here we can
+            #   make pydatetime + Timedelta operations work correctly,
+            #  xref GH#53643
+            td_base = _Timedelta.__new__(cls, milliseconds=value)
+        else:
+            td_base = _Timedelta.__new__(cls, milliseconds=0)
     elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
-        td_base = _Timedelta.__new__(cls, seconds=0)
+        if -86_399_999_913_600 <= value < 86_400_000_000_000:
+            # i.e. we are in range for pytimedelta. By [NAME_REDACTED]
+            #  'correct' value here we can
+            #   make pydatetime + Timedelta operations work correctly,
+            #  xref GH#53643
+            td_base = _Timedelta.__new__(cls, seconds=value)
+        else:
+            td_base = _Timedelta.__new__(cls, seconds=0)
     # Other resolutions are disabled but could potentially be implemented here:
     # elif reso == NPY_DATETIMEUNIT.NPY_FR_m:
     #    td_base = _Timedelta.__new__(Timedelta, minutes=int(value))

@@ -35,6 +35,21 @@ class TestTimedeltaAdditionSubtraction:
         __sub__, __rsub__
     """

+    def test_td_add_sub_pydatetime(self, unit):
+        # GH#53643
+        td = Timedelta(hours=23).as_unit(unit)
+        dt = datetime(2016, 1, 1)
+
+        expected = datetime(2016, 1, 1, 23)
+        result = dt + td
+        assert result == expected
+        result = td + dt
+        assert result == expected
+
+        expected = datetime(2015, 12, 31, 1)
+        result = dt - td
+        assert result == expected
+
     [USER_REDACTED].mark.parametrize(
         "ten_seconds",
         [

Looks good!

```suggestion
        if value >= -86_399_999_913_600_000 and value < 86_400_000_000_000_000:
```

? (the lower bound in https://docs.python.org/3/library/datetime.html#timedelta-objects is also `<=`)

Also, wondering, does `-86_399_999_913_600_000 <= value < 86_400_000_000_000_000` work with cython?

Looks like it does, will push with these edits after running the tests locally.

```suggestion
        if -86_399_999_913_600_000 <= value < 86_400_000_000_000_000:
```

It's only the lower range that is inclusive

```suggestion
        if -86_399_999_913_600 <= value < 86_400_000_000_000:
```


Refactor test_combine_first dataframe

- [X] closes #29562 (Replace xxxx with the GitHub issue number)
- [X] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [X] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

@@ -30,17 +30,19 @@ def test_combine_first_mixed(self):
         combined = f.combine_first(g)
         tm.assert_frame_equal(combined, exp)

-    def test_combine_first(self, float_frame):
-        # disjoint
+    def test_combine_first_disjoint(self, float_frame):
         head, tail = float_frame[:5], float_frame[5:]
-
         combined = head.combine_first(tail)
         reordered_frame = float_frame.reindex(combined.index)
+
         tm.assert_frame_equal(combined, reordered_frame)
         tm.assert_index_equal(combined.columns, float_frame.columns)
         tm.assert_series_equal(combined["A"], reordered_frame["A"])

-        # same index
+        tm.assert_series_equal(combined["A"].reindex(head.index), head["A"])
+        tm.assert_series_equal(combined["A"].reindex(tail.index), tail["A"])
+
+    def test_combine_first_same_index(self, float_frame):
         fcopy = float_frame.copy()
         fcopy["A"] = 1
         del fcopy["C"]
@@ -56,36 +58,36 @@ def test_combine_first(self, float_frame):
         tm.assert_series_equal(combined["C"], fcopy2["C"])
         tm.assert_series_equal(combined["D"], fcopy["D"])

-        # overlap
-        head, tail = reordered_frame[:10].copy(), reordered_frame
+    def test_combine_first_overlap(self, float_frame):
+        combined = float_frame[:5].combine_first(float_frame[5:])
+        reordered_frame = float_frame.reindex(combined.index)
+        head, tail = reordered_frame[:10].copy(), reordered_frame.copy()
         head["A"] = 1
-
         combined = head.combine_first(tail)
         assert (combined["A"][:10] == 1).all()

-        # reverse overlap
+    def test_combine_first_reverse_overlap(self, float_frame):
+        combined = float_frame[:5].combine_first(float_frame[5:])
+        reordered_frame = float_frame.reindex(combined.index)
+        head, tail = reordered_frame[:10].copy(), reordered_frame
+
         tail.iloc[:10, tail.columns.get_loc("A")] = 0
         combined = tail.combine_first(head)
         assert (combined["A"][:10] == 0).all()

-        # no overlap
-        f = float_frame[:10]
-        g = float_frame[10:]
-        combined = f.combine_first(g)
-        tm.assert_series_equal(combined["A"].reindex(f.index), f["A"])
-        tm.assert_series_equal(combined["A"].reindex(g.index), g["A"])
-
-        # corner cases
+    def test_combine_first_with_empty(self, float_frame):
         comb = float_frame.combine_first(DataFrame())
         tm.assert_frame_equal(comb, float_frame)

         comb = DataFrame().combine_first(float_frame)
         tm.assert_frame_equal(comb, float_frame.sort_index())

+    def test_combine_first_with_new_index(self, float_frame):
         comb = float_frame.combine_first(DataFrame(index=["faz", "boo"]))
         assert "faz" in comb.index

-        # #2525
+    def test_combine_first_column_union(self):
+        # GH#2525
         df = DataFrame({"a": [1]}, index=[datetime(2012, 1, 1)])
         df2 = DataFrame(columns=["b"])
         result = df.combine_first(df2)

The no overlap case had the same setup as the disjoint case, just with assertions that had a different focus, so I moved those assertions up into test_combine_first_disjoint and then removed the reverse overlap code.

Could you just inline this logic in the only 2 tests where this is used?


DOC: clarify behavior of shallow copy with Copy-on-Write in NDFrame docstring

closes #63289

@@ -6542,23 +6542,17 @@ def copy(self, deep: bool = True) -> Self:

         When ``deep=False``, a new object will be created without copying
         the calling object's data or index (only references to the data
-        and index are copied). Any changes to the data of the original
-        will be reflected in the shallow copy (and vice versa).
+        and index are copied). With Copy-on-Write, changes to the original
+        will *not* be reflected in the shallow copy (and vice versa). The
+        shallow copy uses a lazy (deferred) copy mechanism that copies the
+        data only when any changes to the original or shallow copy are made,
+        ensuring memory efficiency while maintaining data integrity.

         .. note::
-            The ``deep=False`` behaviour as described above will change
-            in pandas 3.0. `Copy-on-Write
-            <https://pandas.pydata.org/docs/dev/user_guide/copy_on_write.html>`__
-            will be enabled by default, which means that the "shallow" copy
-            is that is returned with ``deep=False`` will still avoid making
-            an eager copy, but changes to the data of the original will *no*
-            longer be reflected in the shallow copy (or vice versa). Instead,
-            it makes use of a lazy (deferred) copy mechanism that will copy
-            the data only when any changes to the original or shallow copy is
-            made.
-
-            You can already get the future behavior and improvements through
-            enabling copy on write ``pd.options.mode.copy_on_write = True``
+            In pandas versions prior to 3.0, the default behavior without
+            Copy-on-Write was different: changes to the original *were* reflected
+            in the shallow copy (and vice versa). See the :ref:`Copy-on-Write
+            user guide <copy_on_write>` for more information.

         Parameters
         ----------

Looking good!

lgtm

[NAME_REDACTED] can't be disabled, I think we should remove the "enabled by default" bit.
```suggestion
        and index are copied). With Copy-on-Write,
```

This is true in terms of mutations at the e.g. `Series` level, but not array.

```python
ser = pd.Series([1, 2, 3])
ser2 = ser.copy(deep=False)
ser.array[1] = 100
print(ser2)
# 0      1
# 1    100
# 2      3
# dtype: int64
```

I'm not sure if we want to highlight this, cc [USER_REDACTED]

I've removed the phase `enabled by default`

Also not sure if we want to go into the details here, but in any case it would be good to slightly rephrase the _"changes to the data of the original will not be reflected .."_ to "changes to the original will not be reflected .." (i.e. leave out the "the data of").

The above example currently also only works with `.array`, and not with eg `.values` (given the current state of https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63099)

I'm actually done with the suggested changes! Please let me know if further improvement is needed...