DOC: Inline docstring in `pandas/core/indexes/multi.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`.

This is a part of https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/62437

@@ -53,7 +53,6 @@
 )
 from pandas.util._decorators import (
     cache_readonly,
-    doc,
     set_module,
 )
 from pandas.util._exceptions import find_stack_level
@@ -122,11 +121,6 @@
         Series,
     )

-_index_doc_kwargs = dict(ibase._index_doc_kwargs)
-_index_doc_kwargs.update(
-    {"klass": "MultiIndex", "target_klass": "MultiIndex or list of tuples"}
-)
-

 class MultiIndexUInt64Engine(libindex.BaseMultiIndexCodesEngine, libindex.UInt64Engine):
     """Manages a MultiIndex by [NAME_REDACTED] positive integers.
@@ -1293,8 +1287,19 @@ def _engine(self):
     def _constructor(self) -> Callable[..., MultiIndex]:  # type: ignore[override]
         return type(self).from_tuples

-    [USER_REDACTED](Index._shallow_copy)
     def _shallow_copy(self, values: np.ndarray, name=lib.no_default) -> MultiIndex:
+        """
+        Create a new Index with the same class as the caller, don't copy the
+        data, use the same object attributes with passed in attributes taking
+        precedence.
+
+        *this is an internal non-public method*
+
+        Parameters
+        ----------
+        values : the values to create the new Index, optional
+        name : Label, defaults to self.name
+        """
         names = name if name is not lib.no_default else self.names

         return type(self).from_tuples(values, sortorder=None, names=names)
@@ -1414,8 +1419,42 @@ def view(self, cls=None) -> Self:
         result._id = self._id
         return result

-    [USER_REDACTED](Index.__contains__)
     def __contains__(self, key: Any) -> bool:
+        """
+        Return a boolean indicating whether the provided key is in the index.
+
+        Parameters
+        ----------
+        key : label
+            The key to check if it is present in the index.
+
+        Returns
+        -------
+        bool
+            Whether the key search is in the index.
+
+        Raises
+        ------
+        TypeError
+            If the key is not hashable.
+
+        See Also
+        --------
+        Index.isin : Returns an ndarray of boolean dtype indicating whether the
+            list-like key is in the index.
+
+        Examples
+        --------
+        >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
+        >>> mi
+        MultiIndex([('a', 'b', 'c')],
+                   )
+
+        >>> "a" in mi
+        True
+        >>> "x" in mi
+        False
+        """
         hash(key)
         try:
             self.get_loc(key)
@@ -1439,8 +1478,37 @@ def f(dtype) -> bool:
         return any(f(level.dtype) for level in self.levels)

     # Cannot determine type of "memory_usage"
-    [USER_REDACTED](Index.memory_usage)  # type: ignore[has-type]
     def memory_usage(self, deep: bool = False) -> int:
+        """
+        Memory usage of the values.
+
+        Parameters
+        ----------
+        deep : bool, default False
+            Introspect the data deeply, interrogate
+            `object` dtypes for system-level memory consumption.
+
+        Returns
+        -------
+        bytes used
+            Returns memory usage of the values in the Index in bytes.
+
+        See Also
+        --------
+        numpy.ndarray.nbytes : Total bytes consumed by [NAME_REDACTED]
+            array.
+
+        Notes
+        -----
+        Memory usage does not include memory consumed by [NAME_REDACTED]
+        are not components of the array if deep=False or if used on PyPy
+
+        Examples
+        --------
+        >>> mi = pd.MultiIndex.from_arrays([["a"], ["b"], ["c"]])
+        >>> mi.memory_usage()
+        81
+        """
         # we are overwriting our base class to avoid
         # computing .values here which could materialize
         # a tuple representation unnecessarily
@@ -1750,8 +1818,61 @@ def is_monotonic_decreasing(self) -> bool:
         # monotonic decreasing if and only if reverse is monotonic increasing
         return self[::-1].is_monotonic_increasing

-    [USER_REDACTED](Index.duplicated)
     def duplicated(self, keep: DropKeep = "first") -> npt.NDArray[np.bool_]:
+        """
+        Indicate duplicate index values.
+
+        Duplicated values are indicated as ``True`` values in the resulting
+        array. Either all duplicates, all except the first, or all except the
+        last occurrence of duplicates can be indicated.
+
+        Parameters
+        ----------
+        keep : {'first', 'last', False}, default 'first'
+            The value or values in a set of duplicates to mark as missing.
+
+            - 'first' : Mark duplicates as ``True`` except for the first
+              occurrence.
+            - 'last' : Mark duplicates as ``True`` except for the last
+              occurrence.
+            - ``False`` : Mark all duplicates as ``True``.
+
+        Returns
+        -------
+        np.ndarray[bool]
+            A numpy array of boolean values indicating duplicate index values.
+
+        See Also
+        --------
+        Series.duplicated : Equivalent method on pandas.Series.
+        DataFrame.duplicated : Equivalent method on pandas.DataFrame.
+        Index.drop_duplicates : Remove duplicate values from Index.
+
+        Examples
+        --------
+        By default, for each set of duplicated values, the first occurrence is
+        set to False and all others to True:
+
+        >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd")))
+        >>> mi.duplicated()
+        array([False, False, False,  True])
+
+        which is equivalent to
+
+        >>> mi.duplicated(keep="first")
+        array([False, False, False,  True])
+
+        By using 'last', the last occurrence of each set of duplicated values
+        is set on False and all others on True:
+
+        >>> mi.duplicated(keep="last")
+        array([ True, False, False, False])
+
+        By [NAME_REDACTED] ``False``, all duplicates are True:
+
+        >>> mi.duplicated(keep=False)
+        array([ True, False, False,  True])
+        """
         shape = tuple(len(lev) for lev in self.levels)
         ids = get_group_index(self.codes, shape, sort=False, xnull=False)

@@ -1767,8 +1888,36 @@ def fillna(self, value):
         """
         raise NotImplementedError("fillna is not defined for MultiIndex")

-    [USER_REDACTED](Index.dropna)
     def dropna(self, how: AnyAll = "any") -> MultiIndex:
+        """
+        [NAME_REDACTED] without NA/NaN values.
+
+        Parameters
+        ----------
+        how : {'any', 'all'}, default 'any'
+            Drop the value when any or all levels are NaN.
+
+        Returns
+        -------
+        Index
+            Returns an MultiIndex object after removing NA/NaN values.
+
+        See Also
+        --------
+        Index.fillna : Fill NA/NaN values with the specified value.
+        Index.isna : Detect missing values.
+
+        Examples
+        --------
+        >>> mi = pd.MultiIndex.from_arrays(([np.nan, np.nan, 2.0], [3.0, np.nan, 4.0]))
+        >>> mi.dropna()
+        MultiIndex([(2.0, 4.0)],
+                   )
+        >>> mi.dropna(how="all")
+        MultiIndex([(nan, 3.0),
+                    (2.0, 4.0)],
+                   )
+        """
         nans = [level_codes == -1 for level_codes in self.codes]
         if how == "any":
             indexer = np.any(nans, axis=0)
@@ -1874,8 +2023,43 @@ def get_level_values(self, level) -> Index:
         values = self._get_level_values(level)
         return values

-    [USER_REDACTED](Index.unique)
     def unique(self, level=None):
+        """
+        Return unique values in the index.
+
+        Unique values are returned in order of appearance, this does NOT sort.
+
+        Parameters
+        ----------
+        level : int or hashable, optional
+            Only return values from specified level (for MultiIndex).
+            If int, gets the level by [NAME_REDACTED], else by [NAME_REDACTED]
+
+        Returns
+        -------
+        MultiIndex
+            Unique values in the MultiIndex.
+
+        See Also
+        --------
+        unique : Numpy array of unique values in that column.
+        Series.unique : Return unique values of Series object.
+
+        Examples
+        --------
+        >>> mi = pd.MultiIndex.from_arrays((list("abca"), list("defd")))
+        >>> mi
+        MultiIndex([('a', 'd'),
+                    ('b', 'e'),
+                    ('c', 'f'),
+                    ('a', 'd')],
+                   )
+        >>> mi.unique()
+        MultiIndex([('a', 'd'),
+                    ('b', 'e'),
+                    ('c', 'f')],
+                   )
+        """
         if level is None:
             return self.drop_duplicates()
         else:
@@ -4165,8 +4349,50 @@ def _convert_can_do_setop(self, other):

     # --------------------------------------------------------------------

-    [USER_REDACTED](Index.astype)
     def astype(self, dtype, copy: bool = True):
+        """
+        Create an MultiIndex with values cast to dtypes.
+
+        The class of a new Index is determined by dtype. When conversion is
+        impossible, a TypeError exception is raised.
+
+        Parameters
+        ----------
+        dtype : numpy dtype or pandas type
+            Note that any signed integer `dtype` is treated as ``'int64'``,
+            and any unsigned integer `dtype` is treated as ``'uint64'``,
+            regardless of the size.
+        copy : bool, default True
+            By default, astype always returns a newly allocated object.
+            If copy is set to False and internal requirements on dtype are
+            satisfied, the original data is used to create a new Index
+            or the original Index is returned.
+
+        Returns
+        -------
+        MultiIndex
+            MultiIndex with values cast to specified dtype.
+
+        See Also
+        --------
+        Index.dtype: Return the dtype object of the underlying data.
+        Index.dtypes: Return the dtype object of the underlying data.
+        Index.convert_dtypes: Convert columns to the best possible dtypes.
+
+        Examples
+        --------
+        >>> mi = pd.MultiIndex.from_arrays(([1, 2, 3], [4, 5, 6]))
+        >>> mi
+        MultiIndex([(1, 4),
+                    (2, 5),
+                    (3, 6)],
+                   )
+        >>> mi.astype("object")
+        MultiIndex([(1, 4),
+                    (2, 5),
+                    (3, 6)],
+                   )
+        """
         dtype = pandas_dtype(dtype)
         if isinstance(dtype, CategoricalDtype):
             msg = "> 1 ndim Categorical are not supported at this time"
@@ -4287,8 +4513,75 @@ def delete(self, loc) -> MultiIndex:
             verify_integrity=False,
         )

-    [USER_REDACTED](Index.isin)
     def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
+        """
+        Return a boolean array where the index values are in `values`.
+
+        Compute boolean array of whether each index value is found in the
+        passed set of values. The length of the returned boolean array matches
+        the length of the index.
+
+        Parameters
+        ----------
+        values : set or list-like
+            Sought values.
+        level : str or int, optional
+            Name or position of the index level to use (if the index is a
+            `MultiIndex`).
+
+        Returns
+        -------
+        np.ndarray[bool]
+            NumPy array of boolean values.
+
+        See Also
+        --------
+        Series.isin : Same for Series.
+        DataFrame.isin : Same method for DataFrames.
+
+        Notes
+        -----
+        In the case of `MultiIndex` you must either specify `values` as a
+        list-like object containing tuples that are the same length as the
+        number of levels, or specify `level`. Otherwise it will raise a
+        ``ValueError``.
+
+        If `level` is specified:
+
+        - if it is the name of one *and only one* index level, use that level;
+        - otherwise it should be a number indicating level position.
+
+        Examples
+        --------
+        >>> idx = pd.Index([1, 2, 3])
+        >>> idx
+        Index([1, 2, 3], dtype='int64')
+
+        Check whether each index value in a list of values.
+
+        >>> idx.isin([1, 4])
+        array([ True, False, False])
+
+        >>> mi = pd.MultiIndex.from_arrays(
+        ...     [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
+        ... )
+        >>> mi
+        MultiIndex([(1,   'red'),
+                    (2,  'blue'),
+                    (3, 'green')],
+                   names=['number', 'color'])
+
+        Check whether the strings in the 'color' level of the MultiIndex
+        are in a list of colors.
+
+        >>> mi.isin(["red", "orange", "yellow"], level="color")
+        array([ True, False, False])
+
+        To check across the levels of a MultiIndex, pass a list of tuples:
+
+        >>> mi.isin([(1, "red"), (3, "red")])
+        array([ True, False, False])
+        """
         if isinstance(values, Generator):
             values = list(values)

For all the `Examples` in this file, could you modify them to show using a `MultiIndex` instead of an `Index`?

sure!


DOC: Inline docstring in `pandas/core/indexes/base.py`

@@ -72,9 +72,7 @@
     Pandas4Warning,
 )
 from pandas.util._decorators import (
-    Appender,
     cache_readonly,
-    doc,
     set_module,
 )
 from pandas.util._exceptions import (
@@ -5023,8 +5021,73 @@ def values(self) -> ArrayLike:
         return data

     [USER_REDACTED]_readonly
-    [USER_REDACTED](IndexOpsMixin.array)
     def array(self) -> ExtensionArray:
+        """
+        The ExtensionArray of the data backing this Index.
+
+        This property provides direct access to the underlying array data of
+        an Index without requiring conversion to a NumPy array. It
+        returns an ExtensionArray, which is the native storage format for
+        pandas extension dtypes.
+
+        Returns
+        -------
+        ExtensionArray
+            An ExtensionArray of the values stored within. For extension
+            types, this is the actual array. For NumPy native types, this
+            is a thin (no copy) wrapper around :class:`numpy.ndarray`.
+
+            ``.array`` differs from ``.values``, which may require converting
+            the data to a different form.
+
+        See Also
+        --------
+        Index.to_numpy : Similar method that always returns a NumPy array.
+        Series.to_numpy : Similar method that always returns a NumPy array.
+
+        Notes
+        -----
+        This table lays out the different array types for each extension
+        dtype within pandas.
+
+        ================== =============================
+        dtype              array type
+        ================== =============================
+        category           Categorical
+        period             PeriodArray
+        interval           IntervalArray
+        [NAME_REDACTED]
+        string             StringArray
+        boolean            BooleanArray
+        datetime64[ns, tz] DatetimeArray
+        ================== =============================
+
+        For any 3rd-party extension types, the array type will be an
+        ExtensionArray.
+
+        For all remaining dtypes ``.array`` will be a
+        :class:`arrays.NumpyExtensionArray` wrapping the actual ndarray
+        stored within. If you absolutely need a NumPy array (possibly with
+        copying / coercing data), then use :meth:`Series.to_numpy` instead.
+
+        Examples
+        --------
+        For regular NumPy types like int, and float, a NumpyExtensionArray
+        is returned.
+
+        >>> pd.Index([1, 2, 3]).array
+        <NumpyExtensionArray>
+        [1, 2, 3]
+        Length: 3, dtype: int64
+
+        For extension types, like Categorical, the actual ExtensionArray
+        is returned
+
+        >>> idx = pd.Index(pd.Categorical(["a", "b", "a"]))
+        >>> idx.array
+        ['a', 'b', 'a']
+        Categories (2, str): ['a', 'b']
+        """
         array = self._data
         if isinstance(array, np.ndarray):
             from pandas.core.arrays.numpy_ import NumpyExtensionArray
@@ -5130,8 +5193,37 @@ def _from_join_target(self, result: np.ndarray) -> ArrayLike:
             return type(self.values)._from_sequence(result, dtype=self.dtype)
         return result

-    [USER_REDACTED](IndexOpsMixin._memory_usage)
     def memory_usage(self, deep: bool = False) -> int:
+        """
+        Memory usage of the values.
+
+        Parameters
+        ----------
+        deep : bool, default False
+            Introspect the data deeply, interrogate
+            `object` dtypes for system-level memory consumption.
+
+        Returns
+        -------
+        bytes used
+            Returns memory usage of the values in the Index in bytes.
+
+        See Also
+        --------
+        numpy.ndarray.nbytes : Total bytes consumed by [NAME_REDACTED]
+            array.
+
+        Notes
+        -----
+        Memory usage does not include memory consumed by [NAME_REDACTED]
+        are not components of the array if deep=False or if used on PyPy
+
+        Examples
+        --------
+        >>> idx = pd.Index([1, 2, 3])
+        >>> idx.memory_usage()
+        24
+        """
         result = self._memory_usage(deep=deep)

         # include our engine hashtable, only if it's already cached
@@ -7469,10 +7561,55 @@ def _maybe_disable_logical_methods(self, opname: str_t) -> None:
         if isinstance(self, ABCMultiIndex):
             raise TypeError(f"cannot perform {opname} with {type(self).__name__}")

-    [USER_REDACTED](IndexOpsMixin.argmin.__doc__)
     def argmin(
         self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs
     ) -> int:
+        """
+        Return int position of the smallest value in the Index.
+
+        If the minimum is achieved in multiple locations,
+        the first row position is returned.
+
+        Parameters
+        ----------
+        axis : None
+            Unused. Parameter needed for compatibility with DataFrame.
+        skipna : bool, default True
+            Exclude NA/null values. If the entire Series is NA, or if ``skipna=False``
+            and there is an NA value, this method will raise a ``ValueError``.
+        *args, **kwargs
+            Additional arguments and keywords for compatibility with NumPy.
+
+        Returns
+        -------
+        int
+            Row position of the minimum value.
+
+        See Also
+        --------
+        Series.argmin : Return position of the minimum value.
+        Series.argmax : Return position of the maximum value.
+        numpy.ndarray.argmin : Equivalent method for numpy arrays.
+        Series.idxmin : Return index label of the minimum values.
+        Series.idxmax : Return index label of the maximum values.
+
+        Examples
+        --------
+        Consider dataset containing cereal calories
+
+        >>> idx = pd.Index([100.0, 110.0, 120.0, 110.0])
+        >>> idx
+        Index([100.0, 110.0, 120.0, 110.0], dtype='float64')
+
+        >>> idx.argmax()
+        np.int64(2)
+        >>> idx.argmin()
+        np.int64(0)
+
+        The maximum cereal calories is the third element and
+        the minimum cereal calories is the first element,
+        since index is zero-indexed.
+        """
         nv.validate_argmin(args, kwargs)
         nv.validate_minmax_axis(axis)

@@ -7484,10 +7621,55 @@ def argmin(

         return super().argmin(skipna=skipna)

-    [USER_REDACTED](IndexOpsMixin.argmax.__doc__)
     def argmax(
         self, axis: AxisInt | None = None, skipna: bool = True, *args, **kwargs
     ) -> int:
+        """
+        Return int position of the largest value in the Index.
+
+        If the maximum is achieved in multiple locations,
+        the first row position is returned.
+
+        Parameters
+        ----------
+        axis : None
+            Unused. Parameter needed for compatibility with DataFrame.
+        skipna : bool, default True
+            Exclude NA/null values. If the entire Series is NA, or if ``skipna=False``
+            and there is an NA value, this method will raise a ``ValueError``.
+        *args, **kwargs
+            Additional arguments and keywords for compatibility with NumPy.
+
+        Returns
+        -------
+        int
+            Row position of the maximum value.
+
+        See Also
+        --------
+        Series.argmax : Return position of the maximum value.
+        Series.argmin : Return position of the minimum value.
+        numpy.ndarray.argmax : Equivalent method for numpy arrays.
+        Series.idxmax : Return index label of the maximum values.
+        Series.idxmin : Return index label of the minimum values.
+
+        Examples
+        --------
+        Consider dataset containing cereal calories
+
+        >>> idx = pd.Index([100.0, 110.0, 120.0, 110.0])
+        >>> idx
+        Index([100.0, 110.0, 120.0, 110.0], dtype='float64')
+
+        >>> idx.argmax()
+        np.int64(2)
+        >>> idx.argmin()
+        np.int64(0)
+
+        The maximum cereal calories is the third element and
+        the minimum cereal calories is the first element,
+        since index is zero-indexed.
+        """
         nv.validate_argmax(args, kwargs)
         nv.validate_minmax_axis(axis)

Could you make these examples show array being called from `Index`?

```suggestion
        Return int position of the smallest value in the Index.
```

Likewise, could you make this show `Index` instead of `Series`?

```suggestion
        Return int position of the largest value in the Index.
```

Same comment about using `Index` here instead

sure!


DOC: Inline docstring in `pandas/core/strings/accessor.py`

@@ -22,7 +22,6 @@
     Scalar,
     npt,
 )
-from pandas.util._decorators import Appender
 from pandas.util._exceptions import find_stack_level

 from pandas.core.dtypes.common import (
@@ -67,7 +66,6 @@
         Series,
     )

-_shared_docs: dict[str, str] = {}
 _cpython_optimized_encoders = (
     "utf-8",
     "utf8",
@@ -699,207 +697,192 @@ def cat(
             out = res_ser.__finalize__(self._orig, method="str_cat")
         return out

-    _shared_docs["str_split"] = r"""
-    Split strings around given separator/delimiter.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def split(
+        self,
+        pat: str | re.Pattern | None = None,
+        *,
+        n=-1,
+        expand: bool = False,
+        regex: bool | None = None,
+    ):
+        r"""
+        Split strings around given separator/delimiter.

-    Splits the string in the Series/Index from the %(side)s,
-    at the specified delimiter string.
+        Splits the string in the Series/Index from the beginning,
+        at the specified delimiter string.

-    Parameters
-    ----------
-    pat : str%(pat_regex)s, optional
-        %(pat_description)s.
-        If not specified, split on whitespace.
-    n : int, default -1 (all)
-        Limit number of splits in output.
-        ``None``, 0 and -1 will be interpreted as return all splits.
-    expand : bool, default False
-        Expand the split strings into separate columns.
-
-        - If ``True``, return DataFrame/MultiIndex expanding dimensionality.
-        - If ``False``, return Series/Index, containing lists of strings.
-    %(regex_argument)s
-    Returns
-    -------
-    Series, Index, DataFrame or MultiIndex
-        Type matches caller unless ``expand=True`` (see Notes).
-    %(raises_split)s
-    See Also
-    --------
-    Series.str.split : Split strings around given separator/delimiter.
-    Series.str.rsplit : Splits string around given separator/delimiter,
-        starting from the right.
-    Series.str.join : Join lists contained as elements in the Series/Index
-        with passed delimiter.
-    str.split : Standard library version for split.
-    str.rsplit : Standard library version for rsplit.
-
-    Notes
-    -----
-    The handling of the `n` keyword depends on the number of found splits:
-
-    - If found splits > `n`,  make first `n` splits only
-    - If found splits <= `n`, make all splits
-    - If for a certain row the number of found splits < `n`,
-      append `None` for padding up to `n` if ``expand=True``
-
-    If using ``expand=True``, Series and Index callers return DataFrame and
-    MultiIndex objects, respectively.
-    %(regex_pat_note)s
-    Examples
-    --------
-    >>> s = pd.Series(
-    ...     [
-    ...         "this is a regular sentence",
-    ...         "https://docs.python.org/3/tutorial/index.html",
-    ...         np.nan
-    ...     ]
-    ... )
-    >>> s
-    0                       this is a regular sentence
-    1    https://docs.python.org/3/tutorial/index.html
-    2                                              NaN
-    dtype: str
+        Parameters
+        ----------
+        pat : str or compiled regex, optional
+            String or regular expression to split on.
+            If not specified, split on whitespace.
+        n : int, default -1 (all)
+            Limit number of splits in output.
+            ``None``, 0 and -1 will be interpreted as return all splits.
+        expand : bool, default False
+            Expand the split strings into separate columns.

-    In the default setting, the string is split by whitespace.
+            - If ``True``, return DataFrame/MultiIndex expanding dimensionality.
+            - If ``False``, return Series/Index, containing lists of strings.

-    >>> s.str.split()
-    0                   [this, is, a, regular, sentence]
-    1    [https://docs.python.org/3/tutorial/index.html]
-    2                                                NaN
-    dtype: object
+        regex : bool, default None
+            Determines if the passed-in pattern is a regular expression:

-    Without the `n` parameter, the outputs of `rsplit` and `split`
-    are identical.
+            - If ``True``, assumes the passed-in pattern is a regular expression
+            - If ``False``, treats the pattern as a literal string.
+            - If ``None`` and `pat` length is 1, treats `pat` as a literal string.
+            - If ``None`` and `pat` length is not 1, treats `pat` as a regular
+              expression.
+            - Cannot be set to False if `pat` is a compiled regex

-    >>> s.str.rsplit()
-    0                   [this, is, a, regular, sentence]
-    1    [https://docs.python.org/3/tutorial/index.html]
-    2                                                NaN
-    dtype: object
+        Returns
+        -------
+        Series, Index, DataFrame or MultiIndex
+            Type matches caller unless ``expand=True`` (see Notes).

-    The `n` parameter can be used to limit the number of splits on the
-    delimiter. The outputs of `split` and `rsplit` are different.
+        Raises
+        ------
+        ValueError
+            * if `regex` is False and `pat` is a compiled regex

-    >>> s.str.split(n=2)
-    0                     [this, is, a regular sentence]
-    1    [https://docs.python.org/3/tutorial/index.html]
-    2                                                NaN
-    dtype: object
+        See Also
+        --------
+        Series.str.split : Split strings around given separator/delimiter.
+        Series.str.rsplit : Splits string around given separator/delimiter,
+            starting from the right.
+        Series.str.join : Join lists contained as elements in the Series/Index
+            with passed delimiter.
+        str.split : Standard library version for split.
+        str.rsplit : Standard library version for rsplit.

-    >>> s.str.rsplit(n=2)
-    0                     [this is a, regular, sentence]
-    1    [https://docs.python.org/3/tutorial/index.html]
-    2                                                NaN
-    dtype: object
+        Notes
+        -----
+        The handling of the `n` keyword depends on the number of found splits:

-    The `pat` parameter can be used to split by [NAME_REDACTED]
+        - If found splits > `n`,  make first `n` splits only
+        - If found splits <= `n`, make all splits
+        - If for a certain row the number of found splits < `n`,
+          append `None` for padding up to `n` if ``expand=True``

-    >>> s.str.split(pat="/")
-    0                         [this is a regular sentence]
-    1    [https:, , docs.python.org, 3, tutorial, index...
-    2                                                  NaN
-    dtype: object
+        If using ``expand=True``, Series and Index callers return DataFrame and
+        MultiIndex objects, respectively.

-    When using ``expand=True``, the split elements will expand out into
-    separate columns. If NaN is present, it is propagated throughout
-    the columns during the split.
-
-    >>> s.str.split(expand=True)
-                                                   0    1    2        3         4
-    0                                           this   is    a  regular  sentence
-    1  https://docs.python.org/3/tutorial/index.html  [NAME_REDACTED]       NaN
-    2                                            [NAME_REDACTED]      [NAME_REDACTED]
-
-    For slightly more complex use cases like splitting the html document name
-    from a url, a combination of parameter settings can be used.
-
-    >>> s.str.rsplit("/", n=1, expand=True)
-                                        0           1
-    0          this is a regular sentence         NaN
-    1  https://docs.python.org/3/tutorial  index.html
-    2                                 [NAME_REDACTED]
-    %(regex_examples)s"""
-
-    [USER_REDACTED](
-        _shared_docs["str_split"]
-        % {
-            "side": "beginning",
-            "pat_regex": " or compiled regex",
-            "pat_description": "String or regular expression to split on",
-            "regex_argument": """
-    regex : bool, default None
-        Determines if the passed-in pattern is a regular expression:
-
-        - If ``True``, assumes the passed-in pattern is a regular expression
-        - If ``False``, treats the pattern as a literal string.
-        - If ``None`` and `pat` length is 1, treats `pat` as a literal string.
-        - If ``None`` and `pat` length is not 1, treats `pat` as a regular expression.
-        - Cannot be set to False if `pat` is a compiled regex
-
-        .. versionadded:: 1.4.0
-         """,
-            "raises_split": """
-                      Raises
-                      ------
-                      ValueError
-                          * if `regex` is False and `pat` is a compiled regex
-                      """,
-            "regex_pat_note": """
-    Use of `regex =False` with a `pat` as a compiled regex will raise an error.
-            """,
-            "method": "split",
-            "regex_examples": r"""
-    Remember to escape special characters when explicitly using regular expressions.
-
-    >>> s = pd.Series(["foo and bar plus baz"])
-    >>> s.str.split(r"and|plus", expand=True)
-        0   1   2
-    0 foo bar baz
-
-    Regular expressions can be used to handle urls or file names.
-    When `pat` is a string and ``regex=None`` (the default), the given `pat` is compiled
-    as a regex only if ``len(pat) != 1``.
-
-    >>> s = pd.Series(['foojpgbar.jpg'])
-    >>> s.str.split(r".", expand=True)
-               0    1
-    0  foojpgbar  jpg
-
-    >>> s.str.split(r"\.jpg", expand=True)
-               0 1
-    0  foojpgbar
-
-    When ``regex=True``, `pat` is interpreted as a regex
-
-    >>> s.str.split(r"\.jpg", regex=True, expand=True)
-               0 1
-    0  foojpgbar
-
-    A compiled regex can be passed as `pat`
-
-    >>> import re
-    >>> s.str.split(re.compile(r"\.jpg"), expand=True)
-               0 1
-    0  foojpgbar
-
-    When ``regex=False``, `pat` is interpreted as the string itself
-
-    >>> s.str.split(r"\.jpg", regex=False, expand=True)
-                   0
-    0  foojpgbar.jpg
-    """,
-        }
-    )
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def split(
-        self,
-        pat: str | re.Pattern | None = None,
-        *,
-        n=-1,
-        expand: bool = False,
-        regex: bool | None = None,
-    ):
+        Use of `regex =False` with a `pat` as a compiled regex will raise an error.
+
+        Examples
+        --------
+        >>> s = pd.Series(
+        ...     [
+        ...         "this is a regular sentence",
+        ...         "https://docs.python.org/3/tutorial/index.html",
+        ...         np.nan,
+        ...     ]
+        ... )
+        >>> s
+        0                       this is a regular sentence
+        1    https://docs.python.org/3/tutorial/index.html
+        2                                              NaN
+        dtype: str
+
+        In the default setting, the string is split by whitespace.
+
+        >>> s.str.split()
+        0                   [this, is, a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object
+
+        Without the `n` parameter, the outputs of `rsplit` and `split`
+        are identical.
+
+        >>> s.str.rsplit()
+        0                   [this, is, a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object
+
+        The `n` parameter can be used to limit the number of splits on the
+        delimiter. The outputs of `split` and `rsplit` are different.
+
+        >>> s.str.split(n=2)
+        0                     [this, is, a regular sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object
+
+        >>> s.str.rsplit(n=2)
+        0                     [this is a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object
+
+        The `pat` parameter can be used to split by [NAME_REDACTED]
+
+        >>> s.str.split(pat="/")
+        0                         [this is a regular sentence]
+        1    [https:, , docs.python.org, 3, tutorial, index...
+        2                                                  NaN
+        dtype: object
+
+        When using ``expand=True``, the split elements will expand out into
+        separate columns. If NaN is present, it is propagated throughout
+        the columns during the split.
+
+        >>> s.str.split(expand=True)
+                                                       0    1    2        3         4
+        0                                           this   is    a  regular  sentence
+        1  https://docs.python.org/3/tutorial/index.html  [NAME_REDACTED]       NaN
+        2                                            [NAME_REDACTED]      [NAME_REDACTED]
+
+        For slightly more complex use cases like splitting the html document name
+        from a url, a combination of parameter settings can be used.
+
+        >>> s.str.rsplit("/", n=1, expand=True)
+                                            0           1
+        0          this is a regular sentence         NaN
+        1  https://docs.python.org/3/tutorial  index.html
+        2                                 [NAME_REDACTED]
+
+        Remember to escape special characters when explicitly using regular expressions.
+
+        >>> s = pd.Series(["foo and bar plus baz"])
+        >>> s.str.split(r"and|plus", expand=True)
+            0   1   2
+        0 foo bar baz
+
+        Regular expressions can be used to handle urls or file names.
+        When `pat` is a string and ``regex=None`` (the default), the given `pat` is
+        compiled as a regex only if ``len(pat) != 1``.
+
+        >>> s = pd.Series(["foojpgbar.jpg"])
+        >>> s.str.split(r".", expand=True)
+                   0    1
+        0  foojpgbar  jpg
+
+        >>> s.str.split(r"\.jpg", expand=True)
+                   0 1
+        0  foojpgbar
+
+        When ``regex=True``, `pat` is interpreted as a regex
+
+        >>> s.str.split(r"\.jpg", regex=True, expand=True)
+                   0 1
+        0  foojpgbar
+
+        A compiled regex can be passed as `pat`
+
+        >>> import re
+        >>> s.str.split(re.compile(r"\.jpg"), expand=True)
+                   0 1
+        0  foojpgbar
+
+        When ``regex=False``, `pat` is interpreted as the string itself
+
+        >>> s.str.split(r"\.jpg", regex=False, expand=True)
+                       0
+        0  foojpgbar.jpg
+        """
         if regex is False and is_re(pat):
             raise ValueError(
                 "Cannot use a compiled regex as replacement pattern with regex=False"
@@ -915,108 +898,134 @@ def split(
             result, expand=expand, returns_string=expand, dtype=dtype
         )

-    [USER_REDACTED](
-        _shared_docs["str_split"]
-        % {
-            "side": "end",
-            "pat_regex": "",
-            "pat_description": "String to split on",
-            "regex_argument": "",
-            "raises_split": "",
-            "regex_pat_note": "",
-            "method": "rsplit",
-            "regex_examples": "",
-        }
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def rsplit(self, pat=None, *, n=-1, expand: bool = False):
-        result = self._data.array._str_rsplit(pat, n=n)
-        dtype = object if self._data.dtype == object else None
-        return self._wrap_result(
-            result, expand=expand, returns_string=expand, dtype=dtype
-        )
+        """
+        Split strings around given separator/delimiter.

-    _shared_docs["str_partition"] = """
-    Split the string at the %(side)s occurrence of `sep`.
+        Splits the string in the Series/Index from the end,
+        at the specified delimiter string.

-    This method splits the string at the %(side)s occurrence of `sep`,
-    and returns 3 elements containing the part before the separator,
-    the separator itself, and the part after the separator.
-    If the separator is not found, return %(return)s.
+        Parameters
+        ----------
+        pat : str, optional
+            String to split on.
+            If not specified, split on whitespace.
+        n : int, default -1 (all)
+            Limit number of splits in output.
+            ``None``, 0 and -1 will be interpreted as return all splits.
+        expand : bool, default False
+            Expand the split strings into separate columns.

-    Parameters
-    ----------
-    sep : str, default whitespace
-        String to split on.
-    expand : bool, default True
-        If True, return DataFrame/MultiIndex expanding dimensionality.
-        If False, return Series/Index.
+            - If ``True``, return DataFrame/MultiIndex expanding dimensionality.
+            - If ``False``, return Series/Index, containing lists of strings.

-    Returns
-    -------
-    DataFrame/MultiIndex or Series/Index of objects
-        Returns appropriate type based on `expand` parameter with strings
-        split based on the `sep` parameter.
+        Returns
+        -------
+        Series, Index, DataFrame or MultiIndex
+            Type matches caller unless ``expand=True`` (see Notes).

-    [NAME_REDACTED]
-    --------
-    %(also)s
-    Series.str.split : Split strings around given separators.
-    str.partition : Standard library version.
+        See Also
+        --------
+        Series.str.split : Split strings around given separator/delimiter.
+        Series.str.rsplit : Splits string around given separator/delimiter,
+            starting from the right.
+        Series.str.join : Join lists contained as elements in the Series/Index
+            with passed delimiter.
+        str.split : Standard library version for split.
+        str.rsplit : Standard library version for rsplit.

-    Examples
-    --------
+        Notes
+        -----
+        The handling of the `n` keyword depends on the number of found splits:

-    >>> s = pd.Series(['Linda van der Berg', '[NAME_REDACTED]'])
-    >>> s
-    0    Linda van der Berg
-    1    [NAME_REDACTED]
-    dtype: str
+        - If found splits > `n`,  make first `n` splits only
+        - If found splits <= `n`, make all splits
+        - If for a certain row the number of found splits < `n`,
+          append `None` for padding up to `n` if ``expand=True``
+
+        If using ``expand=True``, Series and Index callers return DataFrame and
+        MultiIndex objects, respectively.

-    >>> s.str.partition()
-            0  1             2
-    0   Linda     van der Berg
-    1  [NAME_REDACTED]
+        Examples
+        --------
+        >>> s = pd.Series(
+        ...     [
+        ...         "this is a regular sentence",
+        ...         "https://docs.python.org/3/tutorial/index.html",
+        ...         np.nan,
+        ...     ]
+        ... )
+        >>> s
+        0                       this is a regular sentence
+        1    https://docs.python.org/3/tutorial/index.html
+        2                                              NaN
+        dtype: str
+
+        In the default setting, the string is split by whitespace.

-    To partition by [NAME_REDACTED] of the first one:
+        >>> s.str.split()
+        0                   [this, is, a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object

-    >>> s.str.rpartition()
-                   0  1            2
-    0  Linda van der            Berg
-    1         [NAME_REDACTED]
+        Without the `n` parameter, the outputs of `rsplit` and `split`
+        are identical.

-    To partition by [NAME_REDACTED] a space:
+        >>> s.str.rsplit()
+        0                   [this, is, a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object

-    >>> s.str.partition('-')
-                        0  1       2
-    0  Linda van der Berg
-    1         [NAME_REDACTED]  -  Rivers
+        The `n` parameter can be used to limit the number of splits on the
+        delimiter. The outputs of `split` and `rsplit` are different.

-    To return a Series containing tuples instead of a DataFrame:
+        >>> s.str.split(n=2)
+        0                     [this, is, a regular sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object

-    >>> s.str.partition('-', expand=False)
-    0    (Linda van der Berg, , )
-    1    ([NAME_REDACTED], -, Rivers)
-    dtype: object
+        >>> s.str.rsplit(n=2)
+        0                     [this is a, regular, sentence]
+        1    [https://docs.python.org/3/tutorial/index.html]
+        2                                                NaN
+        dtype: object

-    Also available on indices:
+        The `pat` parameter can be used to split by [NAME_REDACTED]

-    >>> idx = pd.Index(['X 123', 'Y 999'])
-    >>> idx
-    Index(['X 123', 'Y 999'], dtype='str')
+        >>> s.str.split(pat="/")
+        0                         [this is a regular sentence]
+        1    [https:, , docs.python.org, 3, tutorial, index...
+        2                                                  NaN
+        dtype: object

-    Which will create a MultiIndex:
+        When using ``expand=True``, the split elements will expand out into
+        separate columns. If NaN is present, it is propagated throughout
+        the columns during the split.

-    >>> idx.str.partition()
-    MultiIndex([('X', ' ', '123'),
-                ('Y', ' ', '999')],
-               )
+        >>> s.str.split(expand=True)
+                                                       0    1    2        3         4
+        0                                           this   is    a  regular  sentence
+        1  https://docs.python.org/3/tutorial/index.html  [NAME_REDACTED]       NaN
+        2                                            [NAME_REDACTED]      [NAME_REDACTED]

-    Or an index with tuples with ``expand=False``:
+        For slightly more complex use cases like splitting the html document name
+        from a url, a combination of parameter settings can be used.

-    >>> idx.str.partition(expand=False)
-    Index([('X', ' ', '123'), ('Y', ' ', '999')], dtype='object')
-    """
+        >>> s.str.rsplit("/", n=1, expand=True)
+                                            0           1
+        0          this is a regular sentence         NaN
+        1  https://docs.python.org/3/tutorial  index.html
+        2                                 [NAME_REDACTED]
+        """
+        result = self._data.array._str_rsplit(pat, n=n)
+        dtype = object if self._data.dtype == object else None
+        return self._wrap_result(
+            result, expand=expand, returns_string=expand, dtype=dtype
+        )

     [USER_REDACTED]_nonstring_types(["bytes"])
     def partition(self, sep: str = " ", expand: bool = True):
@@ -2455,171 +2464,341 @@ def encode(self, encoding, errors: str = "strict"):
         result = self._data.array._str_encode(encoding, errors)
         return self._wrap_result(result, returns_string=False)

-    _shared_docs["str_strip"] = r"""
-    Remove %(position)s characters.
-
-    Strip whitespaces (including newlines) or a set of specified characters
-    from each string in the Series/Index from %(side)s.
-    Replaces any non-strings in Series with NaNs.
-    Equivalent to :meth:`str.%(method)s`.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def strip(self, to_strip=None):
+        """
+        Remove leading and trailing characters.

-    Parameters
-    ----------
-    to_strip : str or None, default None
-        Specifying the set of characters to be removed.
-        All combinations of this set of characters will be stripped.
-        If None then whitespaces are removed.
+        Strip whitespaces (including newlines) or a set of specified characters
+        from each string in the Series/Index from left and right sides.
+        Replaces any non-strings in Series with NaNs.
+        Equivalent to :meth:`str.strip`.

-    Returns
-    -------
-    Series or Index of object
-        Series or Index with the strings being stripped from the %(side)s.
+        Parameters
+        ----------
+        to_strip : str or None, default None
+            Specifying the set of characters to be removed.
+            All combinations of this set of characters will be stripped.
+            If None then whitespaces are removed.

-    [NAME_REDACTED]
-    --------
-    Series.str.strip : Remove leading and trailing characters in Series/Index.
-    Series.str.lstrip : Remove leading characters in Series/Index.
-    Series.str.rstrip : Remove trailing characters in Series/Index.
+        Returns
+        -------
+        Series or Index of object
+            Series or Index with the strings being stripped from the left and
+            right sides.

-    Examples
-    --------
-    >>> s = pd.Series(['1. Ant.  ', '2. Bee!\n', '3. Cat?\t', np.nan, 10, True])
-    >>> s
-    0    1. Ant.
-    1    2. Bee!\n
-    2    3. Cat?\t
-    3          NaN
-    4           10
-    5         True
-    dtype: object
+        See Also
+        --------
+        Series.str.strip : Remove leading and trailing characters in Series/Index.
+        Series.str.lstrip : Remove leading characters in Series/Index.
+        Series.str.rstrip : Remove trailing characters in Series/Index.

-    >>> s.str.strip()
-    0    1. Ant.
-    1    2. Bee!
-    2    3. Cat?
-    3        NaN
-    4        NaN
-    5        NaN
-    dtype: object
+        Examples
+        --------
+        >>> s = pd.Series(["1. Ant.  ", "2. Bee!\\n", "3. Cat?\\t", np.nan, 10, True])
+        >>> s
+        0    1. Ant.
+        1    2. Bee!\\n
+        2    3. Cat?\\t
+        3          NaN
+        4           10
+        5         True
+        dtype: object

-    >>> s.str.lstrip('123.')
-    0    Ant.
-    1    Bee!\n
-    2    Cat?\t
-    3       NaN
-    4       NaN
-    5       NaN
-    dtype: object
+        >>> s.str.strip()
+        0    1. Ant.
+        1    2. Bee!
+        2    3. Cat?
+        3        NaN
+        4        NaN
+        5        NaN
+        dtype: object

-    >>> s.str.rstrip('.!? \n\t')
-    0    1. Ant
-    1    2. Bee
-    2    3. Cat
-    3       NaN
-    4       NaN
-    5       NaN
-    dtype: object
+        >>> s.str.lstrip("123.")
+        0    Ant.
+        1    Bee!\\n
+        2    Cat?\\t
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object

-    >>> s.str.strip('123.!? \n\t')
-    0    Ant
-    1    Bee
-    2    Cat
-    3    NaN
-    4    NaN
-    5    NaN
-    dtype: object
-    """
+        >>> s.str.rstrip(".!? \\n\\t")
+        0    1. Ant
+        1    2. Bee
+        2    3. Cat
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object

-    [USER_REDACTED](
-        _shared_docs["str_strip"]
-        % {
-            "side": "left and right sides",
-            "method": "strip",
-            "position": "leading and trailing",
-        }
-    )
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def strip(self, to_strip=None):
+        >>> s.str.strip("123.!? \\n\\t")
+        0    Ant
+        1    Bee
+        2    Cat
+        3    NaN
+        4    NaN
+        5    NaN
+        dtype: object
+        """
         result = self._data.array._str_strip(to_strip)
         return self._wrap_result(result)

-    [USER_REDACTED](
-        _shared_docs["str_strip"]
-        % {"side": "left side", "method": "lstrip", "position": "leading"}
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def lstrip(self, to_strip=None):
+        """
+        Remove leading characters.
+
+        Strip whitespaces (including newlines) or a set of specified characters
+        from each string in the Series/Index from left side.
+        Replaces any non-strings in Series with NaNs.
+        Equivalent to :meth:`str.lstrip`.
+
+        Parameters
+        ----------
+        to_strip : str or None, default None
+            Specifying the set of characters to be removed.
+            All combinations of this set of characters will be stripped.
+            If None then whitespaces are removed.
+
+        Returns
+        -------
+        Series or Index of object
+            Series or Index with the strings being stripped from the left side.
+
+        See Also
+        --------
+        Series.str.strip : Remove leading and trailing characters in Series/Index.
+        Series.str.lstrip : Remove leading characters in Series/Index.
+        Series.str.rstrip : Remove trailing characters in Series/Index.
+
+        Examples
+        --------
+        >>> s = pd.Series(["1. Ant.  ", "2. Bee!\\n", "3. Cat?\\t", np.nan, 10, True])
+        >>> s
+        0    1. Ant.
+        1    2. Bee!\\n
+        2    3. Cat?\\t
+        3          NaN
+        4           10
+        5         True
+        dtype: object
+
+        >>> s.str.strip()
+        0    1. Ant.
+        1    2. Bee!
+        2    3. Cat?
+        3        NaN
+        4        NaN
+        5        NaN
+        dtype: object
+
+        >>> s.str.lstrip("123.")
+        0    Ant.
+        1    Bee!\\n
+        2    Cat?\\t
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object
+
+        >>> s.str.rstrip(".!? \\n\\t")
+        0    1. Ant
+        1    2. Bee
+        2    3. Cat
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object
+
+        >>> s.str.strip("123.!? \\n\\t")
+        0    Ant
+        1    Bee
+        2    Cat
+        3    NaN
+        4    NaN
+        5    NaN
+        dtype: object
+        """
         result = self._data.array._str_lstrip(to_strip)
         return self._wrap_result(result)

-    [USER_REDACTED](
-        _shared_docs["str_strip"]
-        % {"side": "right side", "method": "rstrip", "position": "trailing"}
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def rstrip(self, to_strip=None):
-        result = self._data.array._str_rstrip(to_strip)
-        return self._wrap_result(result)
+        """
+        Remove trailing characters.

-    _shared_docs["str_removefix"] = r"""
-    Remove a %(side)s from an object series.
+        Strip whitespaces (including newlines) or a set of specified characters
+        from each string in the Series/Index from right side.
+        Replaces any non-strings in Series with NaNs.
+        Equivalent to :meth:`str.rstrip`.

-    If the %(side)s is not present, the original string will be returned.
+        Parameters
+        ----------
+        to_strip : str or None, default None
+            Specifying the set of characters to be removed.
+            All combinations of this set of characters will be stripped.
+            If None then whitespaces are removed.

-    Parameters
-    ----------
-    %(side)s : str
-        Remove the %(side)s of the string.
+        Returns
+        -------
+        Series or Index of object
+            Series or Index with the strings being stripped from the right side.

-    Returns
-    -------
-    Series/Index: object
-        The Series or Index with given %(side)s removed.
+        See Also
+        --------
+        Series.str.strip : Remove leading and trailing characters in Series/Index.
+        Series.str.lstrip : Remove leading characters in Series/Index.
+        Series.str.rstrip : Remove trailing characters in Series/Index.

-    [NAME_REDACTED]
-    --------
-    Series.str.remove%(other_side)s : Remove a %(other_side)s from an object series.
+        Examples
+        --------
+        >>> s = pd.Series(["1. Ant.  ", "2. Bee!\\n", "3. Cat?\\t", np.nan, 10, True])
+        >>> s
+        0    1. Ant.
+        1    2. Bee!\\n
+        2    3. Cat?\\t
+        3          NaN
+        4           10
+        5         True
+        dtype: object

-    Examples
-    --------
-    >>> s = pd.Series(["str_foo", "str_bar", "no_prefix"])
-    >>> s
-    0    str_foo
-    1    str_bar
-    2    no_prefix
-    dtype: str
-    >>> s.str.removeprefix("str_")
-    0    foo
-    1    bar
-    2    no_prefix
-    dtype: str
+        >>> s.str.strip()
+        0    1. Ant.
+        1    2. Bee!
+        2    3. Cat?
+        3        NaN
+        4        NaN
+        5        NaN
+        dtype: object

-    >>> s = pd.Series(["foo_str", "bar_str", "no_suffix"])
-    >>> s
-    0    foo_str
-    1    bar_str
-    2    no_suffix
-    dtype: str
-    >>> s.str.removesuffix("_str")
-    0    foo
-    1    bar
-    2    no_suffix
-    dtype: str
-    """
+        >>> s.str.lstrip("123.")
+        0    Ant.
+        1    Bee!\\n
+        2    Cat?\\t
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object
+
+        >>> s.str.rstrip(".!? \\n\\t")
+        0    1. Ant
+        1    2. Bee
+        2    3. Cat
+        3       NaN
+        4       NaN
+        5       NaN
+        dtype: object
+
+        >>> s.str.strip("123.!? \\n\\t")
+        0    Ant
+        1    Bee
+        2    Cat
+        3    NaN
+        4    NaN
+        5    NaN
+        dtype: object
+        """
+        result = self._data.array._str_rstrip(to_strip)
+        return self._wrap_result(result)

-    [USER_REDACTED](
-        _shared_docs["str_removefix"] % {"side": "prefix", "other_side": "suffix"}
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def removeprefix(self, prefix: str):
+        """
+        Remove a prefix from an object series.
+
+        If the prefix is not present, the original string will be returned.
+
+        Parameters
+        ----------
+        prefix : str
+            Remove the prefix of the string.
+
+        Returns
+        -------
+        Series/Index: object
+            The Series or Index with given prefix removed.
+
+        See Also
+        --------
+        Series.str.removesuffix : Remove a suffix from an object series.
+
+        Examples
+        --------
+        >>> s = pd.Series(["str_foo", "str_bar", "no_prefix"])
+        >>> s
+        0    str_foo
+        1    str_bar
+        2    no_prefix
+        dtype: str
+        >>> s.str.removeprefix("str_")
+        0    foo
+        1    bar
+        2    no_prefix
+        dtype: str
+
+        >>> s = pd.Series(["foo_str", "bar_str", "no_suffix"])
+        >>> s
+        0    foo_str
+        1    bar_str
+        2    no_suffix
+        dtype: str
+        >>> s.str.removesuffix("_str")
+        0    foo
+        1    bar
+        2    no_suffix
+        dtype: str
+        """
         result = self._data.array._str_removeprefix(prefix)
         return self._wrap_result(result)

-    [USER_REDACTED](
-        _shared_docs["str_removefix"] % {"side": "suffix", "other_side": "prefix"}
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def removesuffix(self, suffix: str):
+        """
+        Remove a suffix from an object series.
+
+        If the suffix is not present, the original string will be returned.
+
+        Parameters
+        ----------
+        suffix : str
+            Remove the suffix of the string.
+
+        Returns
+        -------
+        Series/Index: object
+            The Series or Index with given suffix removed.
+
+        See Also
+        --------
+        Series.str.removeprefix : Remove a prefix from an object series.
+
+        Examples
+        --------
+        >>> s = pd.Series(["str_foo", "str_bar", "no_prefix"])
+        >>> s
+        0    str_foo
+        1    str_bar
+        2    no_prefix
+        dtype: str
+        >>> s.str.removeprefix("str_")
+        0    foo
+        1    bar
+        2    no_prefix
+        dtype: str
+
+        >>> s = pd.Series(["foo_str", "bar_str", "no_suffix"])
+        >>> s
+        0    foo_str
+        1    bar_str
+        2    no_suffix
+        dtype: str
+        >>> s.str.removesuffix("_str")
+        0    foo
+        1    bar
+        2    no_suffix
+        dtype: str
+        """
         result = self._data.array._str_removesuffix(suffix)
         return self._wrap_result(result)

@@ -3350,81 +3529,111 @@ def extractall(self, pat, flags: int = 0) -> DataFrame:
         # TODO: dispatch
         return str_extractall(self._orig, pat, flags)

-    _shared_docs["find"] = """
-    Return %(side)s indexes in each strings in the Series/Index.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def find(self, sub, start: int = 0, end=None):
+        """
+        Return lowest indexes in each strings in the Series/Index.

-    Each of returned indexes corresponds to the position where the
-    substring is fully contained between [start:end]. Return -1 on
-    failure. Equivalent to standard :meth:`str.%(method)s`.
+        Each of returned indexes corresponds to the position where the
+        substring is fully contained between [start:end]. Return -1 on
+        failure. Equivalent to standard :meth:`str.find`.

-    Parameters
-    ----------
-    sub : str
-        Substring being searched.
-    start : int
-        Left edge index.
-    end : int
-        Right edge index.
+        Parameters
+        ----------
+        sub : str
+            Substring being searched.
+        start : int
+            Left edge index.
+        end : int
+            Right edge index.

-    Returns
-    -------
-    Series or Index of int.
-        A Series (if the input is a Series) or an Index (if the input is an
-        Index) of the %(side)s indexes corresponding to the positions where the
-        substring is found in each string of the input.
+        Returns
+        -------
+        Series or Index of int.
+            A Series (if the input is a Series) or an Index (if the input is an
+            Index) of the lowest indexes corresponding to the positions where the
+            substring is found in each string of the input.

-    [NAME_REDACTED]
-    --------
-    %(also)s
+        See Also
+        --------
+        rfind : Return highest indexes in each strings.

-    Examples
-    --------
-    For Series.str.find:
-
-    >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
-    >>> ser.str.find("_")
-    0   0
-    1   4
-    2   2
-    dtype: int64
-
-    For Series.str.rfind:
-
-    >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
-    >>> ser.str.rfind("_")
-    0   4
-    1   4
-    2   4
-    dtype: int64
-    """
+        Examples
+        --------
+        For Series.str.find:

-    [USER_REDACTED](
-        _shared_docs["find"]
-        % {
-            "side": "lowest",
-            "method": "find",
-            "also": "rfind : Return highest indexes in each strings.",
-        }
-    )
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def find(self, sub, start: int = 0, end=None):
+        >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
+        >>> ser.str.find("_")
+        0   0
+        1   4
+        2   2
+        dtype: int64
+
+        For Series.str.rfind:
+
+        >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
+        >>> ser.str.rfind("_")
+        0   4
+        1   4
+        2   4
+        dtype: int64
+        """
         if not isinstance(sub, str):
             msg = f"expected a string object, not {type(sub).__name__}"
             raise TypeError(msg)

         result = self._data.array._str_find(sub, start, end)
         return self._wrap_result(result, returns_string=False)

-    [USER_REDACTED](
-        _shared_docs["find"]
-        % {
-            "side": "highest",
-            "method": "rfind",
-            "also": "find : Return lowest indexes in each strings.",
-        }
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def rfind(self, sub, start: int = 0, end=None):
+        """
+        Return highest indexes in each strings in the Series/Index.
+
+        Each of returned indexes corresponds to the position where the
+        substring is fully contained between [start:end]. Return -1 on
+        failure. Equivalent to standard :meth:`str.rfind`.
+
+        Parameters
+        ----------
+        sub : str
+            Substring being searched.
+        start : int
+            Left edge index.
+        end : int
+            Right edge index.
+
+        Returns
+        -------
+        Series or Index of int.
+            A Series (if the input is a Series) or an Index (if the input is an
+            Index) of the highest indexes corresponding to the positions where the
+            substring is found in each string of the input.
+
+        See Also
+        --------
+        find : Return lowest indexes in each strings.
+
+        Examples
+        --------
+        For Series.str.find:
+
+        >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
+        >>> ser.str.find("_")
+        0   0
+        1   4
+        2   2
+        dtype: int64
+
+        For Series.str.rfind:
+
+        >>> ser = pd.Series(["_cow_", "duck_", "do_v_e"])
+        >>> ser.str.rfind("_")
+        0   4
+        1   4
+        2   4
+        dtype: int64
+        """
         if not isinstance(sub, str):
             msg = f"expected a string object, not {type(sub).__name__}"
             raise TypeError(msg)
@@ -3472,84 +3681,113 @@ def normalize(self, form):
         result = self._data.array._str_normalize(form)
         return self._wrap_result(result)

-    _shared_docs["index"] = """
-    Return %(side)s indexes in each string in Series/Index.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def index(self, sub, start: int = 0, end=None):
+        """
+        Return lowest indexes in each string in Series/Index.
+
+        Each of the returned indexes corresponds to the position where the
+        substring is fully contained between [start:end]. This is the same
+        as ``str.find`` except instead of returning -1, it raises a
+        ValueError when the substring is not found. Equivalent to standard
+        ``str.index``.

-    Each of the returned indexes corresponds to the position where the
-    substring is fully contained between [start:end]. This is the same
-    as ``str.%(similar)s`` except instead of returning -1, it raises a
-    ValueError when the substring is not found. Equivalent to standard
-    ``str.%(method)s``.
+        Parameters
+        ----------
+        sub : str
+            Substring being searched.
+        start : int
+            Left edge index.
+        end : int
+            Right edge index.

-    Parameters
-    ----------
-    sub : str
-        Substring being searched.
-    start : int
-        Left edge index.
-    end : int
-        Right edge index.
+        Returns
+        -------
+        Series or Index of object
+            Returns a Series or an Index of the lowest indexes
+            in each string of the input.

-    Returns
-    -------
-    Series or Index of object
-        Returns a Series or an Index of the %(side)s indexes
-        in each string of the input.
+        See Also
+        --------
+        rindex : Return highest indexes in each strings.

-    [NAME_REDACTED]
-    --------
-    %(also)s
+        Examples
+        --------
+        For Series.str.index:

-    Examples
-    --------
-    For Series.str.index:
-
-    >>> ser = pd.Series(["horse", "eagle", "donkey"])
-    >>> ser.str.index("e")
-    0   4
-    1   0
-    2   4
-    dtype: int64
-
-    For Series.str.rindex:
-
-    >>> ser = pd.Series(["Deer", "eagle", "Sheep"])
-    >>> ser.str.rindex("e")
-    0   2
-    1   4
-    2   3
-    dtype: int64
-    """
+        >>> ser = pd.Series(["horse", "eagle", "donkey"])
+        >>> ser.str.index("e")
+        0   4
+        1   0
+        2   4
+        dtype: int64

-    [USER_REDACTED](
-        _shared_docs["index"]
-        % {
-            "side": "lowest",
-            "similar": "find",
-            "method": "index",
-            "also": "rindex : Return highest indexes in each strings.",
-        }
-    )
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def index(self, sub, start: int = 0, end=None):
+        For Series.str.rindex:
+
+        >>> ser = pd.Series(["Deer", "eagle", "Sheep"])
+        >>> ser.str.rindex("e")
+        0   2
+        1   4
+        2   3
+        dtype: int64
+        """
         if not isinstance(sub, str):
             msg = f"expected a string object, not {type(sub).__name__}"
             raise TypeError(msg)

         result = self._data.array._str_index(sub, start=start, end=end)
         return self._wrap_result(result, returns_string=False)

-    [USER_REDACTED](
-        _shared_docs["index"]
-        % {
-            "side": "highest",
-            "similar": "rfind",
-            "method": "rindex",
-            "also": "index : Return lowest indexes in each strings.",
-        }
-    )
     [USER_REDACTED]_nonstring_types(["bytes"])
     def rindex(self, sub, start: int = 0, end=None):
+        """
+        Return highest indexes in each string in Series/Index.
+
+        Each of the returned indexes corresponds to the position where the
+        substring is fully contained between [start:end]. This is the same
+        as ``str.rfind`` except instead of returning -1, it raises a
+        ValueError when the substring is not found. Equivalent to standard
+        ``str.rindex``.
+
+        Parameters
+        ----------
+        sub : str
+            Substring being searched.
+        start : int
+            Left edge index.
+        end : int
+            Right edge index.
+
+        Returns
+        -------
+        Series or Index of object
+            Returns a Series or an Index of the highest indexes
+            in each string of the input.
+
+        See Also
+        --------
+        index : Return lowest indexes in each strings.
+
+        Examples
+        --------
+        For Series.str.index:
+
+        >>> ser = pd.Series(["horse", "eagle", "donkey"])
+        >>> ser.str.index("e")
+        0   4
+        1   0
+        2   4
+        dtype: int64
+
+        For Series.str.rindex:
+
+        >>> ser = pd.Series(["Deer", "eagle", "Sheep"])
+        >>> ser.str.rindex("e")
+        0   2
+        1   4
+        2   3
+        dtype: int64
+        """
         if not isinstance(sub, str):
             msg = f"expected a string object, not {type(sub).__name__}"
             raise TypeError(msg)

Could you remove this `.. versionadded`? Generally we want to remove these that reference a version less than 2.0


BUG: fix to_json with non-nano timedelta index + avoid casting to ns for datetime-like

Closes https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63236

Changes in the PR:

- Support passing the unit if the int value in the C `int64ToIsoDuration` helper for timedelta conversion, similarly as is done in `int64ToIso` for datetime conversion
- in ObjToJSON C code:
  - for conversion to "iso", now always pass the value unit as well, for both timedelta and datetime (so in this case, no up front casting to nanoseconds is needed, as the ISO conversion will handle that correctly)
  - for conversion to "epoch", convert to the desired unit up front (before going into the C code) in python, such that in the C code we can just use the integer value (and don't have to deal with the explosion of any unit -> any unit conversion in the C code). This adds some additional python code, but since "epoch" is deprecated, this can be removed in the future.
- Added some more code comments while trying to understand what is going on (and renamed a few variables to have clearer names)

Because the ISO path handles unit conversion itself, and for epoch I added the conversion up front in python, we can AFAIK remove the cast to nanoseconds that was added in https://github.com/[USER_REDACTED]ndas-dev/pandas/pull/63100 (this was also only done for the columns values and not row/column indexes, which caused the bug this issue is closing)

@@ -22,4 +22,5 @@ int scaleNanosecToUnit(int64_t *value, NPY_DATETIMEUNIT unit);
 char *int64ToIso(int64_t value, NPY_DATETIMEUNIT valueUnit,
                  NPY_DATETIMEUNIT base, size_t *len);

-char *int64ToIsoDuration(int64_t value, size_t *len);
+char *int64ToIsoDuration(int64_t value, NPY_DATETIMEUNIT valueUnit,
+                         size_t *len);

@@ -37,7 +37,7 @@ typedef struct {
   char *(*int64ToIso)(int64_t, NPY_DATETIMEUNIT, NPY_DATETIMEUNIT, size_t *);
   char *(*PyDateTimeToIso)(PyObject *, NPY_DATETIMEUNIT, size_t *);
   npy_datetime (*PyDateTimeToEpoch)(PyObject *, NPY_DATETIMEUNIT);
-  char *(*int64ToIsoDuration)(int64_t, size_t *);
+  char *(*int64ToIsoDuration)(int64_t, NPY_DATETIMEUNIT, size_t *);
   void (*pandas_datetime_to_datetimestruct)(npy_datetime, NPY_DATETIMEUNIT,
                                             npy_datetimestruct *);
   void (*pandas_timedelta_to_timedeltastruct)(npy_datetime, NPY_DATETIMEUNIT,
@@ -80,8 +80,8 @@ static PandasDateTime_CAPI *PandasDateTimeAPI = NULL;
     PandasDateTimeAPI->PyDateTimeToIso((obj), (base), (len))
 #  define PyDateTimeToEpoch(dt, base)                                          \
     PandasDateTimeAPI->PyDateTimeToEpoch((dt), (base))
-#  define int64ToIsoDuration(value, len)                                       \
-    PandasDateTimeAPI->int64ToIsoDuration((value), (len))
+#  define int64ToIsoDuration(value, valueUnit, len)                            \
+    PandasDateTimeAPI->int64ToIsoDuration((value), (valueUnit), (len))
 #  define pandas_datetime_to_datetimestruct(dt, base, out)                     \
     PandasDateTimeAPI->pandas_datetime_to_datetimestruct((dt), (base), (out))
 #  define pandas_timedelta_to_timedeltastruct(td, base, out)                   \

@@ -70,11 +70,12 @@ char *int64ToIso(int64_t value, NPY_DATETIMEUNIT valueUnit,
 }

 /* Converts the int64_t representation of a duration to ISO; mutates len */
-char *int64ToIsoDuration(int64_t value, size_t *len) {
+char *int64ToIsoDuration(int64_t value, NPY_DATETIMEUNIT valueUnit,
+                         size_t *len) {
   pandas_timedeltastruct tds;
   int ret_code;

-  pandas_timedelta_to_timedeltastruct(value, NPY_FR_ns, &tds);
+  pandas_timedelta_to_timedeltastruct(value, valueUnit, &tds);

   // Max theoretical length of ISO Duration with 64 bit day
   // as the largest unit is 70 characters + 1 for a null terminator

@@ -131,8 +131,13 @@ typedef struct __PyObjectEncoder {
   int npyType;
   void *npyValue;

+  // whether to encode datetime as ISO strings (0=epoch int, 1=ISO str)
   int datetimeIso;
+  // the unit to encode to, governs epoch and ISO8601 precision (default ms)
   NPY_DATETIMEUNIT datetimeUnit;
+  // pass-through: the unit of the actual integer value being encoded
+  // (has to be set when calling NpyDateTimeToIsoCallback or
+  // NpyTimeDeltaToIsoCallback)
   NPY_DATETIMEUNIT valueUnit;

   // output format style for pandas data types
@@ -335,7 +340,8 @@ static const char *NpyDateTimeToIsoCallback(JSOBJ Py_UNUSED(unused),
 /* JSON callback. returns a char* and mutates the pointer to *len */
 static const char *NpyTimeDeltaToIsoCallback(JSOBJ Py_UNUSED(unused),
                                              JSONTypeContext *tc, size_t *len) {
-  GET_TC(tc)->cStr = int64ToIsoDuration(GET_TC(tc)->longValue, len);
+  NPY_DATETIMEUNIT valueUnit = ((PyObjectEncoder *)tc->encoder)->valueUnit;
+  GET_TC(tc)->cStr = int64ToIsoDuration(GET_TC(tc)->longValue, valueUnit, len);
   return GET_TC(tc)->cStr;
 }

@@ -1242,7 +1248,7 @@ static char **NpyArr_encodeLabels(PyArrayObject *labels, PyObjectEncoder *enc,
                                   npy_intp num) {
   // NOTE this function steals a reference to labels.
   PyObject *item = NULL;
-  const NPY_DATETIMEUNIT base = enc->datetimeUnit;
+  const NPY_DATETIMEUNIT targetUnit = enc->datetimeUnit;

   if (!labels) {
     return 0;
@@ -1280,13 +1286,16 @@ static char **NpyArr_encodeLabels(PyArrayObject *labels, PyObjectEncoder *enc,
     }

     int is_datetimelike = 0;
+    int from_numpy = 0;
     int64_t i8date;
-    NPY_DATETIMEUNIT dateUnit = NPY_FR_ns;
+    NPY_DATETIMEUNIT valueUnit = NPY_FR_ns;
     if (PyTypeNum_ISDATETIME(type_num)) {
       is_datetimelike = 1;
+      from_numpy = 1;
       i8date = *(int64_t *)dataptr;
-      dateUnit = get_datetime_metadata_from_dtype(dtype).base;
+      valueUnit = get_datetime_metadata_from_dtype(dtype).base;
     } else if (PyDate_Check(item) || PyDelta_Check(item)) {
+      // convert datetime/timedelta object to nanos
       is_datetimelike = 1;
       if (PyObject_HasAttrString(item, "_value")) {
         // pd.Timestamp object or pd.NaT
@@ -1316,13 +1325,12 @@ static char **NpyArr_encodeLabels(PyArrayObject *labels, PyObjectEncoder *enc,
       } else {
         if (enc->datetimeIso) {
           if ((type_num == NPY_TIMEDELTA) || (PyDelta_Check(item))) {
-            // TODO(username): non-nano timedelta support?
-            cLabel = int64ToIsoDuration(i8date, &len);
+            cLabel = int64ToIsoDuration(i8date, valueUnit, &len);
           } else {
             if (type_num == NPY_DATETIME) {
-              cLabel = int64ToIso(i8date, dateUnit, base, &len);
+              cLabel = int64ToIso(i8date, valueUnit, targetUnit, &len);
             } else {
-              cLabel = PyDateTimeToIso(item, base, &len);
+              cLabel = PyDateTimeToIso(item, targetUnit, &len);
             }
           }
           if (cLabel == NULL) {
@@ -1334,10 +1342,14 @@ static char **NpyArr_encodeLabels(PyArrayObject *labels, PyObjectEncoder *enc,
         } else {
           int size_of_cLabel = 21; // 21 chars for int 64
           cLabel = PyObject_Malloc(size_of_cLabel);
-          if (scaleNanosecToUnit(&i8date, base) == -1) {
-            NpyArr_freeLabels(ret, num);
-            ret = 0;
-            break;
+          if (!from_numpy) {
+            // numpy arrays are already scaled to the correct unit
+            // only need to scale if coming from a datetime/timedelta object
+            if (scaleNanosecToUnit(&i8date, targetUnit) == -1) {
+              NpyArr_freeLabels(ret, num);
+              ret = 0;
+              break;
+            }
           }
           snprintf(cLabel, size_of_cLabel, "%" PRId64, i8date);
           len = strlen(cLabel);
@@ -1440,10 +1452,8 @@ static void Object_beginTypeContext(JSOBJ _obj, JSONTypeContext *tc) {
         pc->longValue = longVal;
         tc->type = JT_UTF8;
       } else {
-        NPY_DATETIMEUNIT base = ((PyObjectEncoder *)tc->encoder)->datetimeUnit;
-        if (scaleNanosecToUnit(&longVal, base) == -1) {
-          goto INVALID;
-        }
+        // numpy array was already scaled to unit, so just use int value for
+        // epoch
         pc->longValue = longVal;
         tc->type = JT_LONG;
       }
@@ -1567,6 +1577,7 @@ static void Object_beginTypeContext(JSOBJ _obj, JSONTypeContext *tc) {
       return;
     } else if (enc->datetimeIso) {
       pc->PyTypeToUTF8 = NpyTimeDeltaToIsoCallback;
+      enc->valueUnit = NPY_FR_ns;
       tc->type = JT_UTF8;
     } else {
       const int unit = ((PyObjectEncoder *)tc->encoder)->datetimeUnit;

@@ -2479,9 +2479,6 @@ def _with_freq(self, freq) -> Self:

     def _values_for_json(self) -> np.ndarray:
         # Small performance bump vs the base class which calls np.asarray(self)
-        if self.unit != "ns":
-            # GH#55827
-            return self.as_unit("ns")._values_for_json()
         if isinstance(self.dtype, np.dtype):
             return self._ndarray
         return super()._values_for_json()

@@ -2613,7 +2613,7 @@ def to_json(
         elif date_format is None:
             date_format = "epoch"
             dtypes = self.dtypes if self.ndim == 2 else [self.dtype]
-            if any(lib.is_np_dtype(dtype, "mM") for dtype in dtypes):
+            if any(dtype.kind in "mM" for dtype in dtypes):
                 warnings.warn(
                     "The default 'epoch' date format is deprecated and will be removed "
                     "in a future version, please use 'iso' date format instead.",

@@ -186,6 +186,38 @@ def to_json(
     if orient == "table" and isinstance(obj, Series):
         obj = obj.to_frame(name=obj.name or "values")

+    if date_format == "epoch":
+        # for epoch (numeric) format, convert datetime-likes to the desired
+        # unit up front, such that the C ObjToJSON code can simply write out
+        # the integer values without worrying about conversion
+        if date_unit not in ["s", "ms", "us", "ns"]:
+            raise ValueError(f"Invalid value '{date_unit}' for option 'date_unit'")
+        if isinstance(obj, DataFrame):
+            copied = False
+            cols = np.nonzero(obj.dtypes.map(lambda dt: dt.kind in ["M", "m"]))[0]
+            if len(cols):
+                obj = obj.copy(deep=False)
+                copied = True
+                for col in cols:
+                    obj.isetitem(col, obj.iloc[:, col].dt.as_unit(date_unit))
+            if obj.index.dtype.kind in "Mm":
+                if not copied:
+                    obj = obj.copy(deep=False)
+                    copied = True
+                obj.index = Series(obj.index).dt.as_unit(date_unit)
+            if obj.columns.dtype.kind in "Mm":
+                if not copied:
+                    obj = obj.copy(deep=False)
+                    copied = True
+                obj.columns = Series(obj.columns).dt.as_unit(date_unit)
+        elif isinstance(obj, Series):
+            if obj.dtype.kind in "Mm":
+                obj = obj.copy(deep=False)
+                obj = obj.dt.as_unit(date_unit)
+            if obj.index.dtype.kind in "Mm":
+                obj = obj.copy(deep=False)
+                obj.index = Series(obj.index).dt.as_unit(date_unit)
+
     writer: type[Writer]
     if orient == "table" and isinstance(obj, DataFrame):
         writer = JSONTableWriter

@@ -153,12 +153,10 @@ def test_frame_non_unique_columns(self, orient, data, request):
             expected = DataFrame(data)
             if expected.iloc[:, 0].dtype == "datetime64[s]":
                 # orient == "values" by [NAME_REDACTED] objects out
-                # in milliseconds; these are internally stored in nanosecond,
-                # so divide to get where we need
+                # in milliseconds;
                 # TODO: a to_epoch method would also solve; see GH 14772
                 dta = expected.iloc[:, 0]._values
-                dta = dta.as_unit("ns")  # GH#55827
-                expected.isetitem(0, dta.astype(np.int64) // 1_000_000)
+                expected.isetitem(0, dta.as_unit("ms").astype(np.int64))
         elif orient == "split":
             expected = df
             expected.columns = ["x", "x.1"]
@@ -273,12 +271,21 @@ def test_roundtrip_empty(self, orient, convert_axes):

         tm.assert_frame_equal(result, expected)

+    [USER_REDACTED].mark.parametrize(
+        "dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
+    )
     [USER_REDACTED].mark.parametrize("convert_axes", [True, False])
-    def test_roundtrip_timestamp(self, orient, convert_axes, datetime_frame):
+    def test_roundtrip_timestamp(
+        self, orient, convert_axes, dtype_backend, datetime_frame
+    ):
         # TODO: improve coverage with date_format parameter
+        expected = datetime_frame.copy()
+        if dtype_backend is not None:
+            datetime_frame.index = Series(datetime_frame.index).convert_dtypes(
+                dtype_backend=dtype_backend
+            )
         data = StringIO(datetime_frame.to_json(orient=orient))
         result = read_json(data, orient=orient, convert_axes=convert_axes)
-        expected = datetime_frame.copy()

         if not convert_axes:  # one off for ts handling
             # DTI gets converted to epoch values
@@ -736,11 +743,19 @@ def test_series_roundtrip_empty(self, orient):

         tm.assert_series_equal(result, expected)

-    def test_series_roundtrip_timeseries(self, orient, datetime_series):
+    [USER_REDACTED].mark.parametrize(
+        "dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
+    )
+    def test_series_roundtrip_timeseries(self, dtype_backend, orient, datetime_series):
+        expected = datetime_series.copy()
+        if dtype_backend is not None:
+            datetime_series.index = Series(datetime_series.index).convert_dtypes(
+                dtype_backend=dtype_backend
+            )
+
         data = StringIO(datetime_series.to_json(orient=orient))
         result = read_json(data, typ="series", orient=orient)

-        expected = datetime_series
         if orient in ("values", "records"):
             expected = expected.reset_index(drop=True)
         else:
@@ -780,15 +795,20 @@ def test_series_with_dtype(self):
         expected = Series([4] * 3)
         tm.assert_series_equal(result, expected)

+    [USER_REDACTED].mark.parametrize(
+        "dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
+    )
     [USER_REDACTED].mark.parametrize(
         "dtype,expected",
         [
             (True, Series(["2000-01-01"], dtype="datetime64[ms]")),
             (False, Series([946684800000])),
         ],
     )
-    def test_series_with_dtype_datetime(self, dtype, expected):
+    def test_series_with_dtype_datetime(self, dtype, expected, dtype_backend):
         s = Series(["2000-01-01"], dtype="datetime64[ns]")
+        if dtype_backend is not None:
+            s = s.convert_dtypes(dtype_backend=dtype_backend)
         msg = (
             "The default 'epoch' date format is deprecated and will be removed "
             "in a future version, please use 'iso' date format instead."
@@ -1052,8 +1072,11 @@ def test_default_epoch_date_format_deprecated(self, df, warn):
         with tm.assert_produces_warning(warn, match=msg):
             df.to_json()

+    [USER_REDACTED].mark.parametrize(
+        "dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
+    )
     [USER_REDACTED].mark.parametrize("unit", ["s", "ms", "us"])
-    def test_iso_non_nano_datetimes(self, unit):
+    def test_iso_non_nano_datetimes(self, dtype_backend, unit):
         # Test that numpy datetimes
         # in an Index or a column with non-nano resolution can be serialized
         # correctly
@@ -1076,20 +1099,17 @@ def test_iso_non_nano_datetimes(self, unit):
                 ),
             },
         )
+        expected = df.copy()
+        expected.index = expected.index.as_unit("us")
+        expected["date"] = expected["date"].dt.as_unit("us")
+        expected["date_obj"] = expected["date_obj"].astype("datetime64[us]")

-        buf = StringIO()
-        df.to_json(buf, date_format="iso", date_unit=unit)
-        buf.seek(0)
+        if dtype_backend is not None:
+            df = df.convert_dtypes(dtype_backend=dtype_backend)

-        # read_json always reads datetimes in nanosecond resolution
-        # TODO: check_dtype/check_index_type should be removable
-        # once read_json gets non-nano support
-        tm.assert_frame_equal(
-            read_json(buf, convert_dates=["date", "date_obj"]),
-            df,
-            check_index_type=False,
-            check_dtype=False,
-        )
+        json = df.to_json(date_format="iso", date_unit=unit)
+        result = read_json(StringIO(json), convert_dates=["date", "date_obj"])
+        tm.assert_frame_equal(result, expected)

     def test_weird_nested_json(self):
         # this used to core dump the parser
@@ -1154,11 +1174,11 @@ def test_url(self, httpserver):
         ]:
             assert result[field].dtype == dtype

-    def test_timedelta(self):
+    def test_timedelta(self, unit):
         converter = lambda x: pd.to_timedelta(x, unit="ms")

-        ser = Series([timedelta(23), timedelta(seconds=5)], dtype="m8[ns]")
-        assert ser.dtype == "timedelta64[ns]"
+        ser = Series([timedelta(23), timedelta(seconds=5)], dtype=f"m8[{unit}]")
+        assert ser.dtype == f"timedelta64[{unit}]"

         msg = (
             "The default 'epoch' date format is deprecated and will be removed "
@@ -1816,13 +1836,32 @@ def test_read_json_with_very_long_file_path(self, compression):
             # _get_data_from_filepath()
             read_json(long_json_path)

+    [USER_REDACTED].mark.parametrize(
+        "date_format,key", [("epoch", 86400000), ("iso", "1970-01-02T00:00:00.000")]
+    )
+    def test_datetime_as_label(self, date_format, key, unit):
+        df = DataFrame(
+            [[1]], columns=[(Timestamp(0) + pd.Timedelta("1D")).as_unit(unit)]
+        )
+        expected = f'{{"{key}":{{"0":1}}}}'
+
+        expected_warning = None
+        if date_format == "epoch":
+            expected_warning = Pandas4Warning
+
+        msg = (
+            "'epoch' date format is deprecated and will be removed in a future "
+            "version, please use 'iso' date format instead."
+        )
+        with tm.assert_produces_warning(expected_warning, match=msg):
+            result = df.to_json(date_format=date_format)
+
+        assert result == expected
+
     [USER_REDACTED].mark.parametrize(
         "date_format,key", [("epoch", 86400000), ("iso", "P1DT0H0M0S")]
     )
-    def test_timedelta_as_label(self, date_format, key, unit, request):
-        if unit != "ns":
-            mark = pytest.mark.xfail(reason="GH#63236 failure to round-trip")
-            request.applymarker(mark)
+    def test_timedelta_as_label(self, date_format, key, unit):
         df = DataFrame([[1]], columns=[pd.Timedelta("1D").as_unit(unit)])
         expected = f'{{"{key}":{{"0":1}}}}'

1) dtype.kind in "mM" is _slightly_ faster
2) will this be wrong for pyarrow date dtype?

Good point, the basic Index with ArrowDtype won't have `as_unit` directly. Pushed a fix for this (and test coverage), not sure if there is an easier way right now than roundtripping through series to use `.dt.unit()`? (it's a bit ugly, but converting to datetime/timdelta index and back would be even more verbose)

could just do `isinstance(obj._values, (DatetimeArray, TimedeltaArray))` for now?

Not really, because I now changed the C code to assume the datetimes-like data are already cast to the desired unit up front, and it is no longer done in the C code. So not handling the ArrowDtype variant here would actually mean writing wrong values for those dtypes (some of the tests I added after your previous comment to parametrized with pyarrow dtypes fail if doing this change).


CLN: Enable ruff rule PLR1714 (repeated-equality-comparison)

- [ ] 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.
- [x] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

@@ -2280,7 +2280,7 @@ def _repr_categories(self) -> list[str]:
         from pandas.io.formats import format as fmt

         formatter = None
-        if self.categories.dtype == "str" or self.categories.dtype == "string":
+        if self.categories.dtype == "str" or self.categories.dtype == "string":  # noqa: PLR1714 (repeated-equality-comparison)
             # the extension array formatter defaults to boxed=True in format_array
             # override here to boxed=False to be consistent with QUOTE_NONNUMERIC
             formatter = cast(ExtensionArray, self.categories._values)._formatter(

@@ -238,7 +238,7 @@ def __eq__(self, other: object) -> bool:
         # cannot be checked with normal `==`
         if isinstance(other, str):
             # TODO should dtype == "string" work for the NaN variant?
-            if other == "string" or other == self.name:
+            if other == "string" or other == self.name:  # noqa: PLR1714 (repeated-equality-comparison)
                 return True
             try:
                 other = self.construct_from_string(other)

@@ -129,7 +129,7 @@ def _astype_nansafe(
         )
         raise ValueError(msg)

-    if copy or arr.dtype == object or dtype == object:
+    if copy or object in (arr.dtype, dtype):
         # Explicit copy, or required since NumPy can't view from / to object.
         return arr.astype(dtype, copy=True)

@@ -12147,7 +12147,7 @@ def join(

             # join indexes only using concat
             if can_concat:
-                if how == "left" or how == "right":
+                if how in {"left", "right"}:
                     res = concat(
                         frames, axis=1, join="outer", verify_integrity=True, sort=sort
                     )

@@ -461,8 +461,7 @@ def newfunc(values: np.ndarray, *, axis: AxisInt | None = None, **kwargs):
             # only takes this path for wide arrays (long dataframes), for threshold see
             # https://github.com/[USER_REDACTED]ndas-dev/pandas/pull/43311#issuecomment-974891737
             and (values.shape[1] / 1000) > values.shape[0]
-            and values.dtype != object
-            and values.dtype != bool
+            and values.dtype not in (object, bool)
         ):
             arrs = list(values)
             if kwargs.get("mask") is not None:

@@ -303,7 +303,7 @@ def test_wrap_agg_out(three_group):
     grouped = three_group.groupby(["A", "B"])

     def func(ser):
-        if ser.dtype == object or ser.dtype == "string":
+        if ser.dtype in (object, "string"):
             raise TypeError("Test error message")
         return ser.sum()

@@ -48,7 +48,7 @@ def test_map_identity_mapping(index, request, using_infer_string):
         request.applymarker(mark)

     result = index.map(lambda x: x)
-    if index.dtype == object and (result.dtype == bool or result.dtype == "string"):
+    if index.dtype == object and (result.dtype in (bool, "string")):
         assert (index == result).all()
         # TODO: could work that into the 'exact="equiv"'?
         return  # FIXME: doesn't belong in this file anymore!

@@ -456,7 +456,7 @@ def test_insert_out_of_bounds(self, index, using_infer_string):
             msg = "slice indices must be integers or None or have an __index__ method"

         if using_infer_string:
-            if index.dtype == "string" or index.dtype == "category":
+            if str(index.dtype) in {"str", "string", "category"}:
                 msg = "loc must be an integer between"
             elif index.dtype == "object" and len(index) == 0:
                 msg = "loc must be an integer between"

@@ -403,7 +403,7 @@ def test_boxplot_group_no_xlabel_ylabel(self, vert, request):
         for subplot in ax:
             target_label = (
                 subplot.get_xlabel()
-                if vert == {"vert": True} or vert == {"orientation": "vertical"}
+                if vert in ({"vert": True}, {"orientation": "vertical"})
                 else subplot.get_ylabel()
             )
             assert target_label == pprint_thing(["group"])

@@ -350,7 +350,7 @@ def test_str_cat_datetime_index_unsorted(join_type):
     s = Series(["a", "b"], index=[values[1], values[0]])
     others = Series(["c", "d"], index=[values[0], values[1]])
     result = s.str.cat(others, join=join_type)
-    if join_type == "outer" or join_type == "right":
+    if join_type in {"outer", "right"}:
         expected = Series(["bc", "ad"], index=[values[0], values[1]])
     else:
         expected = Series(["ad", "bc"], index=[values[1], values[0]])

@@ -339,8 +339,6 @@ ignore = [
   "RUF012",
   # type-comparison
   "E721",
-  # repeated-equality-comparison
-  "PLR1714",
   # self-or-cls-assignment
   "PLW0642",
   # literal-membership

Could we use a noqa for this one?

Could we use a noqa for this one?