BUG: handle negative overflow in Period conversion from timestamp

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

@@ -1307,6 +1307,7 @@ I/O
 Period
 ^^^^^^
 - Fixed error message when passing invalid period alias to :meth:`PeriodIndex.to_timestamp` (:issue:`58974`)
+- Fixed recoverable overflow for near-minimum timestamps with microsecond resolution (:issue:`63278`)

 Plotting
 ^^^^^^^^

@@ -455,8 +455,20 @@ npy_datetime npy_datetimestruct_to_datetime(NPY_DATETIMEUNIT base,
   }

   int64_t microseconds;
-  PD_CHECK_OVERFLOW(scaleSecondsToMicroseconds(seconds, &microseconds));
-  PD_CHECK_OVERFLOW(checked_add(microseconds, dts->us, &microseconds));
+  const int64_t us_per_second = 1000000L;
+  const int64_t min_scalable_seconds = NPY_MIN_INT64 / us_per_second;
+  if (seconds < min_scalable_seconds) {
+    const int64_t seconds_below_threshold = seconds - min_scalable_seconds;
+    // split y = a * x + b, where a * x underflows into two parts.
+    // y = a * (x + k - k) + b = a * (x + k) + (b - a * k), where k > 0.
+    int64_t scaled_us = us_per_second * (seconds - seconds_below_threshold);
+    PD_CHECK_OVERFLOW(checked_add(us_per_second * seconds_below_threshold,
+                                  dts->us, &microseconds));
+    PD_CHECK_OVERFLOW(checked_add(scaled_us, microseconds, &microseconds));
+  } else {
+    PD_CHECK_OVERFLOW(scaleSecondsToMicroseconds(seconds, &microseconds));
+    PD_CHECK_OVERFLOW(checked_add(microseconds, dts->us, &microseconds));
+  }

   if (base == NPY_FR_us) {
     return microseconds;

@@ -169,6 +169,22 @@ def test_construction_from_timestamp_nanos(self):
         rt2 = per2.to_timestamp()
         assert rt2.asm8 == dt64

+    def test_construction_from_min_timestamp(self):
+        # GH-63278
+        ts = Timestamp(Timestamp.min.value, unit="us")
+        per = Period(ts, freq="us")
+
+        # pandas.errors.OutOfBoundsDatetime: Out of bounds nanosecond timestamp:
+        # -290308-12-21 19:59:05
+        # rt = per.to_timestamp()
+
+        assert per.year == ts.year
+        assert per.month == ts.month
+        assert per.day == ts.day
+        assert per.hour == ts.hour
+        assert per.minute == ts.minute
+        assert per.second == ts.second
+
     def test_construction_bday(self):
         # Biz day construction, roll forward if non-weekday
         with tm.assert_produces_warning(

Is there a test that can hit either of these overflow checks?

The test that I created hits this branch, with `seconds_below_threshold = -1`. It uses the minimum possible timestamp built with microseconds resolution. One value below it is `NaT`, and above can't enter this branch.

I don't know, on the top of my head, how would I create a valid `datetimestruct` that provokes an overflow in this branch.


CoW: better chained assignment warning message for update() method

Addresses https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/59788

@@ -140,7 +140,7 @@ def raises_chained_assignment_error(extra_warnings=(), extra_match=()):
     else:
         warning = ChainedAssignmentError
         match = (
-            "A value is trying to be set on a copy of a DataFrame or Series "
+            "A value is being set on a copy of a DataFrame or Series "
             "through chained assignment"
         )
         if extra_warnings:

@@ -62,7 +62,7 @@
     Pandas4Warning,
 )
 from pandas.errors.cow import (
-    _chained_assignment_method_msg,
+    _chained_assignment_method_update_msg,
     _chained_assignment_msg,
 )
 from pandas.util._decorators import (
@@ -10540,7 +10540,7 @@ def update(
                 self
             ) <= REF_COUNT_METHOD and not com.is_local_in_caller_frame(self):
                 warnings.warn(
-                    _chained_assignment_method_msg,
+                    _chained_assignment_method_update_msg,
                     ChainedAssignmentError,
                     stacklevel=2,
                 )

@@ -47,7 +47,7 @@
     Pandas4Warning,
 )
 from pandas.errors.cow import (
-    _chained_assignment_method_msg,
+    _chained_assignment_method_update_msg,
     _chained_assignment_msg,
 )
 from pandas.util._decorators import (
@@ -3521,7 +3521,7 @@ def update(self, other: Series | Sequence | Mapping) -> None:
                 self
             ) <= REF_COUNT_METHOD and not com.is_local_in_caller_frame(self):
                 warnings.warn(
-                    _chained_assignment_method_msg,
+                    _chained_assignment_method_update_msg,
                     ChainedAssignmentError,
                     stacklevel=2,
                 )

@@ -1,5 +1,5 @@
 _chained_assignment_msg = (
-    "A value is trying to be set on a copy of a DataFrame or Series "
+    "A value is being set on a copy of a DataFrame or Series "
     "through chained assignment.\n"
     "Such chained assignment never works to update the original DataFrame or "
     "Series, because the intermediate object on which we are setting values "
@@ -13,7 +13,7 @@

 _chained_assignment_method_msg = (
-    "A value is trying to be set on a copy of a DataFrame or Series "
+    "A value is being set on a copy of a DataFrame or Series "
     "through chained assignment using an inplace method.\n"
     "Such inplace method never works to update the original DataFrame or Series, "
     "because the intermediate object on which we are setting values always "
@@ -26,3 +26,18 @@
     "https://pandas.pydata.org/pandas-docs/stable/user_guide/"
     "copy_on_write.html"
 )
+
+
+_chained_assignment_method_update_msg = (
+    "A value is being set on a copy of a DataFrame or Series "
+    "through chained assignment using an inplace method.\n"
+    "Such inplace method never works to update the original DataFrame or Series, "
+    "because the intermediate object on which we are setting values always "
+    "behaves as a copy (due to Copy-on-Write).\n\n"
+    "For example, when doing 'df[col].update(other)', try "
+    "using 'df.update({col: other})' instead, to perform "
+    "the operation inplace on the original object.\n\n"
+    "See the documentation for a more detailed explanation: "
+    "https://pandas.pydata.org/pandas-docs/stable/user_guide/"
+    "copy_on_write.html"
+)

@@ -14,8 +14,6 @@
 )
 import pandas._testing as tm

-msg = "A value is trying to be set on a copy of a slice from a DataFrame"
-

 class TestCaching:
     [USER_REDACTED].mark.parametrize("do_ref", [True, False])

@@ -50,6 +50,7 @@
     "_global_config",
     "_chained_assignment_msg",
     "_chained_assignment_method_msg",
+    "_chained_assignment_method_update_msg",
     "_version_meson",
     # The numba extensions need this to mock the iloc object
     "_iLocIndexer",

I would be inclined to remove this example (with the intermediate variable) and only show `df.update({col: other})` instead (although that only works for Series.update, not generally for DataFrame.update)

Agree with the removal.

I'd also suggest removing a lot of details from the message and adding them to a section of the user guide instead, linking directly to that section. But not strongly so.

Could you clarify which details? The actual example code, or the general message at the beginning?
(because that might also apply to the generic message used for all other methods)

```suggestion
    "A value is being set on a copy of a DataFrame or Series "
    "through chained assignment using an inplace method and will not modify "
    "the original. See https://pandas.pydata.org/pandas-docs/stable/user_guide/"
    "copy_on_write.html#[some_ref] for more details."
```

With the details I am removing here being added a a subsection of these docs.

Added a suggestion in the comment below. The motivation is just to keep the warning message short, but as mentioned don't feel strongly here.

Already going to merge this with just the second example replaced, for the sake to at least have a better message in the release (for rewriting it more fully, would also have to update the other messages)


API: update read_json to follow general default of microseconds

- [x] closes #63442
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

This updates `read_json` to:

- follow the general default of microseconds (or nanos if needed for the precision of the string) when parsing string columns
- follow the actual (inferred) date unit of the numeric input (by default `to_json` writes milliseconds), similarly as is now done in `to_datetime`

@@ -16,6 +16,7 @@
     final,
     overload,
 )
+import warnings

 import numpy as np

@@ -28,7 +29,10 @@
 )
 from pandas._libs.tslibs import iNaT
 from pandas.compat._optional import import_optional_dependency
-from pandas.errors import AbstractMethodError
+from pandas.errors import (
+    AbstractMethodError,
+    OutOfBoundsDatetime,
+)
 from pandas.util._decorators import set_module
 from pandas.util._validators import check_dtype_backend

@@ -1292,10 +1296,7 @@ def _try_convert_to_date(self, data: Series) -> Series:

         new_data = data

-        if new_data.dtype == "string":
-            new_data = new_data.astype(object)
-
-        if new_data.dtype == "object":
+        if new_data.dtype == "object" or new_data.dtype == "string":  # noqa: PLR1714
             try:
                 new_data = data.astype("int64")
             except OverflowError:
@@ -1313,18 +1314,32 @@ def _try_convert_to_date(self, data: Series) -> Series:
             if not in_range.all():
                 return data

-        date_units = (self.date_unit,) if self.date_unit else self._STAMP_UNITS
-        for date_unit in date_units:
-            try:
-                # In case of multiple possible units, infer the likely unit
-                # based on the first unit for which the parsed dates fit
-                # within the nanoseconds bounds
-                # -> do as_unit cast to ensure OutOfBounds error
-                return to_datetime(new_data, errors="raise", unit=date_unit).dt.as_unit(
-                    "ns"
-                )
-            except (ValueError, OverflowError, TypeError):
-                continue
+        if new_data.dtype == "string":
+            with warnings.catch_warnings():
+                # ignore "Could not infer format" warnings from to_datetime
+                # which is incorrectly raised for non-date strings
+                warnings.simplefilter("ignore", UserWarning)
+                for format in (None, "iso8601", "mixed"):
+                    try:
+                        return to_datetime(new_data, errors="raise", format=format)
+                    except Exception:
+                        pass
+        else:
+            # numeric or mixed objects
+            date_units = (self.date_unit,) if self.date_unit else self._STAMP_UNITS
+            for date_unit in date_units:
+                try:
+                    # In case of multiple possible units, infer the likely unit
+                    # based on the first unit for which the parsed dates fit
+                    # within the nanoseconds bounds
+                    # -> do as_unit cast to ensure OutOfBounds error
+                    data = to_datetime(new_data, errors="raise", unit=date_unit)
+                    _ = data.dt.as_unit("ns")
+                    break
+                except OutOfBoundsDatetime:
+                    continue
+                except (ValueError, OverflowError, TypeError):
+                    pass
         return data

@@ -163,7 +163,7 @@ def test_frame_non_unique_columns(self, orient, data, request):
             expected = df
             expected.columns = ["x", "x.1"]
             if expected["x"].dtype.kind == "M":
-                expected["x"] = expected["x"].astype("M8[ns]")  # GH#55827
+                expected["x"] = expected["x"].astype("M8[ms]")

         tm.assert_frame_equal(result, expected)

@@ -287,6 +287,8 @@ def test_roundtrip_timestamp(self, orient, convert_axes, datetime_frame):
                 idx = idx.astype(str)

             expected.index = idx
+        else:
+            expected.index = expected.index.as_unit("ms")

         assert_json_roundtrip_equal(result, expected, orient)

@@ -514,13 +516,19 @@ def test_v12_compat(self, datapath):

         dirpath = datapath("io", "json", "data")
         v12_json = os.path.join(dirpath, "tsframe_v012.json")
-        df_unser = read_json(v12_json)
-        tm.assert_frame_equal(df, df_unser)
+        result = read_json(v12_json)
+        expected = df.copy()
+        expected["date"] = expected["date"].dt.as_unit("ns")
+        expected["modified"] = expected["modified"].dt.as_unit("ns")
+        expected.index = expected.index.as_unit("ns")
+        tm.assert_frame_equal(result, expected)

-        df_iso = df.drop(["modified"], axis=1)
         v12_iso_json = os.path.join(dirpath, "tsframe_iso_v012.json")
-        df_unser_iso = read_json(v12_iso_json)
-        tm.assert_frame_equal(df_iso, df_unser_iso, check_column_type=False)
+        result_iso = read_json(v12_iso_json)
+        expected_iso = df.drop(["modified"], axis=1)
+        expected_iso["date"] = expected_iso["date"].dt.as_unit("us")
+        expected_iso.index = expected_iso.index.as_unit("us")
+        tm.assert_frame_equal(result_iso, expected_iso, check_column_type=False)

     def test_blocks_compat_GH9037(self, using_infer_string):
         index = date_range("20000101", periods=10, freq="h", unit="ns")
@@ -611,6 +619,7 @@ def test_blocks_compat_GH9037(self, using_infer_string):
         )
         data = StringIO(df_mixed.to_json(orient="split"))
         df_roundtrip = read_json(data, orient="split")
+        df_mixed.index = df_mixed.index.as_unit("ms")
         tm.assert_frame_equal(
             df_mixed,
             df_roundtrip,
@@ -734,6 +743,8 @@ def test_series_roundtrip_timeseries(self, orient, datetime_series):
         expected = datetime_series
         if orient in ("values", "records"):
             expected = expected.reset_index(drop=True)
+        else:
+            expected.index = expected.index.as_unit("ms")
         if orient != "split":
             expected.name = None

@@ -772,7 +783,7 @@ def test_series_with_dtype(self):
     [USER_REDACTED].mark.parametrize(
         "dtype,expected",
         [
-            (True, Series(["2000-01-01"], dtype="datetime64[ns]")),
+            (True, Series(["2000-01-01"], dtype="datetime64[ms]")),
             (False, Series([946684800000])),
         ],
     )
@@ -815,12 +826,16 @@ def test_axis_dates(self, datetime_series, datetime_frame):
         # frame
         json = StringIO(datetime_frame.to_json())
         result = read_json(json)
-        tm.assert_frame_equal(result, datetime_frame)
+        expected = datetime_frame.copy()
+        expected.index = expected.index.as_unit("ms")
+        tm.assert_frame_equal(result, expected)

         # series
         json = StringIO(datetime_series.to_json())
         result = read_json(json, typ="series")
-        tm.assert_series_equal(result, datetime_series, check_names=False)
+        expected = datetime_series.copy()
+        expected.index = expected.index.as_unit("ms")
+        tm.assert_series_equal(result, expected, check_names=False)
         assert result.name is None

     def test_convert_dates(self, datetime_series, datetime_frame):
@@ -835,7 +850,10 @@ def test_convert_dates(self, datetime_series, datetime_frame):
         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             json = StringIO(df.to_json())
         result = read_json(json)
-        tm.assert_frame_equal(result, df)
+        expected = df.copy()
+        expected["date"] = expected["date"].dt.as_unit("ms")
+        expected.index = expected.index.as_unit("ms")
+        tm.assert_frame_equal(result, expected)

         df["foo"] = 1.0
         with tm.assert_produces_warning(Pandas4Warning, match=msg):
@@ -852,7 +870,9 @@ def test_convert_dates(self, datetime_series, datetime_frame):
         with tm.assert_produces_warning(Pandas4Warning, match=msg):
             json = StringIO(ts.to_json())
         result = read_json(json, typ="series")
-        tm.assert_series_equal(result, ts)
+        expected = ts.dt.as_unit("ms")
+        expected.index = expected.index.as_unit("ms")
+        tm.assert_series_equal(result, expected)

     [USER_REDACTED].mark.parametrize("date_format", ["epoch", "iso"])
     [USER_REDACTED].mark.parametrize("as_object", [True, False])
@@ -908,7 +928,7 @@ def test_convert_dates_infer(self, infer_word):
         expected = DataFrame(
             [[1, Timestamp("2002-11-08")], [2, pd.NaT]], columns=["id", infer_word]
         )
-        expected[infer_word] = expected[infer_word].astype("M8[ns]")
+        expected[infer_word] = expected[infer_word].astype("M8[ms]")

         result = read_json(StringIO(ujson_dumps(data)))[["id", infer_word]]
         tm.assert_frame_equal(result, expected)
@@ -936,6 +956,9 @@ def test_date_format_frame(self, date, date_unit, datetime_frame):

         result = read_json(StringIO(json))
         expected = df.copy()
+        if date_unit != "ns":
+            expected["date"] = expected["date"].dt.as_unit("us")
+            expected.index = expected.index.as_unit("us")
         tm.assert_frame_equal(result, expected)

     def test_date_format_frame_raises(self, datetime_frame):
@@ -964,7 +987,9 @@ def test_date_format_series(self, date, date_unit, datetime_series):
             json = ts.to_json(date_format="iso")

         result = read_json(StringIO(json), typ="series")
-        expected = ts.copy().dt.as_unit("ns")
+        expected = ts.copy()
+        if date_unit != "ns":
+            expected.index = expected.index.as_unit("us")
         tm.assert_series_equal(result, expected)

     def test_date_format_series_raises(self, datetime_series):
@@ -989,12 +1014,15 @@ def test_date_unit(self, unit, datetime_frame):
             json = df.to_json(date_format="epoch", date_unit=unit)

         # force date unit
-        result = read_json(StringIO(json), date_unit=unit)
-        tm.assert_frame_equal(result, df)
+        # result = read_json(StringIO(json), date_unit=unit)
+        expected = df.copy()
+        expected["date"] = expected["date"].dt.as_unit(unit)
+        expected.index = expected.index.as_unit(unit)
+        # tm.assert_frame_equal(result, expected)

         # detect date unit
         result = read_json(StringIO(json), date_unit=None)
-        tm.assert_frame_equal(result, df)
+        tm.assert_frame_equal(result, expected)

     [USER_REDACTED].mark.parametrize(
         "df, warn",
@@ -1115,19 +1143,16 @@ def test_round_trip_exception(self, datapath):

     [USER_REDACTED].mark.network
     [USER_REDACTED].mark.single_cpu
-    [USER_REDACTED].mark.parametrize(
-        "field,dtype",
-        [
-            ["created_at", pd.DatetimeTZDtype(tz="UTC")],
-            ["closed_at", "datetime64[ns]"],
-            ["updated_at", pd.DatetimeTZDtype(tz="UTC")],
-        ],
-    )
-    def test_url(self, field, dtype, httpserver):
+    def test_url(self, httpserver):
         data = '{"created_at": ["2023-06-23T18:21:36Z"], "closed_at": ["2023-06-23T18:21:36"], "updated_at": ["2023-06-23T18:21:36Z"]}\n'  # noqa: E501
         httpserver.serve_content(content=data)
         result = read_json(httpserver.url, convert_dates=True)
-        assert result[field].dtype == dtype
+        for field, dtype in [
+            ["created_at", pd.DatetimeTZDtype("us", tz="UTC")],
+            ["closed_at", "datetime64[us]"],
+            ["updated_at", pd.DatetimeTZDtype("us", tz="UTC")],
+        ]:
+            assert result[field].dtype == dtype

     def test_timedelta(self):
         converter = lambda x: pd.to_timedelta(x, unit="ms")
@@ -1585,7 +1610,7 @@ def test_to_json_from_json_columns_dtypes(self, orient, using_infer_string):
                 "Bool": Series([True, False, True], dtype="bool"),
                 "Category": Series(["a", "b", None], dtype="category"),
                 "Datetime": Series(
-                    ["2020-01-01", None, "2020-01-03"], dtype="datetime64[ns]"
+                    ["2020-01-01", None, "2020-01-03"], dtype="datetime64[ms]"
                 ),
             }
         )
@@ -1757,7 +1782,7 @@ def test_read_timezone_information(self):
         result = read_json(
             StringIO('{"2019-01-01T11:00:00.000Z":88}'), typ="series", orient="index"
         )
-        exp_dti = DatetimeIndex(["2019-01-01 11:00:00"], dtype="M8[ns, UTC]")
+        exp_dti = DatetimeIndex(["2019-01-01 11:00:00"], dtype="M8[us, UTC]")
         expected = Series([88], index=exp_dti)
         tm.assert_series_equal(result, expected)

@@ -8,7 +8,6 @@
 import pytest

 from pandas.compat.pyarrow import pa_version_under17p0
-from pandas.errors import Pandas4Warning

 from pandas import (
     DataFrame,
@@ -81,12 +80,7 @@ def test_to_read_gcs(gcs_buffer, format, monkeypatch, capsys, request):
         df1.to_excel(path)
         df2 = read_excel(path, parse_dates=["dt"], index_col=0)
     elif format == "json":
-        msg = (
-            "The default 'epoch' date format is deprecated and will be removed "
-            "in a future version, please use 'iso' date format instead."
-        )
-        with tm.assert_produces_warning(Pandas4Warning, match=msg):
-            df1.to_json(path)
+        df1.to_json(path, date_format="iso")
         df2 = read_json(path, convert_dates=["dt"])
     elif format == "parquet":
         pytest.importorskip("pyarrow")
@@ -118,7 +112,7 @@ def from_uri(path):
         df2 = df1

     expected = df1[:]
-    if format in ["csv", "excel"]:
+    if format in ["csv", "excel", "json"]:
         expected["dt"] = expected["dt"].dt.as_unit("us")

     tm.assert_frame_equal(df2, expected)

would this PR be simplified if we wait until I can put in a PR changing the round-float behavior?

why is the format bit necessary in this PR but not in main?

As the comment suggests, my assumption is that this can be removed and thus indeed simplified if we make sure to_datetime handles this. So a PR for that is certainly welcome

It was only to after making the main change that I noticed that we had tests covering floats, so added this temporary block to make the tests passing

Because on main we are specifying a unit, also when parsing strings, and that causes to_datetime to take a different more flexible code path.

I initially thought to separate the numeric vs string case, to make it clearer what is happening (eg testing with multiple units is not needed for strings). But this required those additional changes for the string case.
I could also revert that (but I would say that is messy either way ...)

should now be removable following #63595

IIUC in main we're assuming we only get here with integers, while you're adding coverage for strings?  im fine with that in principle, but ideally would prefer to keep this PR focused

id expect this to change to "us" instead of "ms".  looks like the same applies to a few changed tests below.  whats the new rule of thumb?

Done!

> IIUC in main we're assuming we only get here with integers, while you're adding coverage for strings?

No, this `_try_convert_to_date` gets used for trying to convert both string or numeric datetime-like json data

The top post of the PR mentions the following rule of thumb for the changes:

- follow the general default of microseconds (or nanos if needed for the precision of the string) when parsing string columns
- follow the actual (inferred) date unit of the numeric input (by default `to_json` writes milliseconds), similarly as is now done in `to_datetime`

Is that clear (and does that make sense)?

So I thought because here we are parsing numeric data as milliseconds, I would also output "ms" unit, to follow the change we made in `to_datetime`

(this should be documented, though)

> Is that clear (and does that make sense)?

Yes, thanks.

this path is for object dtype.  is it possible to get integers here?  e.g. `np.array([0, 1], dtype=object)`?

We get here after normal DataFrame/Series inference of the parsed data, so if it is only integers like your example, that should be inferred properly and not get here as object dtype.

But, you could of course have mixed data .. e.g. json like `'{"date":{"0":0,"1":"2012-01-01"}}'`, then we would end up doing essentially `pd.to_datetime([1, "2012-01-01"])` for the column.
And at that point, it actually does give different results depending on what you pass to `to_datetime` ...

```
>>> pd.to_datetime([1, "2012-01-01"])
DatetimeIndex(['1970-01-01 00:00:00.000000001', '2012-01-01 00:00:00'], dtype='datetime64[ns]', freq=None)
>>> pd.to_datetime([1, "2012-01-01"], unit="s")
DatetimeIndex(['1970-01-01 00:00:01', '2012-01-01 00:00:00'], dtype='datetime64[us]', freq=None)
```

Maybe I should just keep the object dtype parsing closer to how it was before (and not get the faster parsing for the default ISO case)

> Maybe I should just keep the object dtype parsing closer to how it was before (and not get the faster parsing for the default ISO case)

I think the ISO case in particular might be OK because the presence of an int would cause it to raise

```
>>> pd.to_datetime([1, "2016-01-01"], format="ISO8601")
ValueError: Time data 1 is not ISO8601 format. You might want to try:
```

In general though, I'd advocate keeping this PR minimal.


ENH: [NAME_REDACTED] to work with object dtype

### I am AI 🤖 : Code is changed by [NAME_REDACTED], I read agents.md, ensuring that every changed line is reviewed 😉.

## Summary
Fixes #63444 - [NAME_REDACTED]() to work with object dtype.

## Example

```python
# Before (raised error)
s = pd.Series([1.234, 2.567], dtype=object)
s.round(2)  # ❌ TypeError

# After (works)
s = pd.Series([1.234, 2.567], dtype=object)
s.round(2)  # ✅ Series([1.23, 2.57], dtype=object)

# Also works with Decimal
from decimal import Decimal
s = pd.Series([Decimal("1.234")], dtype=object)
s.round(2)  # ✅ Series([Decimal("1.23")], dtype=object)
```

## Changes

Modified Series.round() to handle object dtype using lib.map_infer for element-wise rounding
Added dtype validation to raise TypeError for non-numeric, non-object dtypes
Added comprehensive tests
lib-map-infer approach added

## [NAME_REDACTED]
### Screenshots

## [NAME_REDACTED] breaking changes - existing numeric dtype behavior unchanged.

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

@@ -267,6 +267,7 @@ Other enhancements
 - Added :meth:`DataFrame.from_arrow` and :meth:`Series.from_arrow` to import any Arrow-compatible
   data object into a pandas object through the
   `[NAME_REDACTED] <https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html>`__ (:issue:`59631`)
+- :meth:`Series.round` now supports object dtypes when the underlying Python objects implement ``__round__`` (:issue:`63444`)

 .. ---------------------------------------------------------------------------
 .. _whatsnew_300.notable_bug_fixes:

@@ -2577,9 +2577,18 @@ def round(self, decimals: int = 0, *args, **kwargs) -> Series:
         4    3.0
         dtype: float64
         """
+
         nv.validate_round(args, kwargs)
-        if self.dtype == "object":
-            raise TypeError("Expected numeric dtype, got object instead.")
+
+        if len(self) == 0:
+            return self.copy()
+
+        if is_object_dtype(self.dtype):
+            values = self._values
+            result = lib.map_infer(values, lambda x: round(x, decimals), convert=False)
+            return self._constructor(result, index=self.index, copy=False).__finalize__(
+                self, method="round"
+            )
         new_mgr = self._mgr.round(decimals=decimals)
         return self._constructor_from_mgr(new_mgr, axes=new_mgr.axes).__finalize__(
             self, method="round"

@@ -73,9 +73,47 @@ def test_round_ea_boolean(self):
         result.iloc[0] = False
         tm.assert_series_equal(ser, expected)

-    def test_round_dtype_object(self):
-        # GH#61206
-        ser = Series([0.2], dtype="object")
-        msg = "Expected numeric dtype, got object instead."
-        with pytest.raises(TypeError, match=msg):
-            ser.round()
+    [USER_REDACTED].mark.parametrize(
+        "data,decimals,expected_data",
+        [
+            ([0.2], 0, [0.0]),
+            ([1.234, 2.567], 1, [1.2, 2.6]),
+            ([1.234, 2.567], 2, [1.23, 2.57]),
+            ([1.234, 2.567], 0, [1.0, 3.0]),
+        ],
+    )
+    def test_round_dtype_object(self, data, decimals, expected_data):
+        # GH#63444
+        ser = Series(data, dtype="object")
+        result = ser.round(decimals)
+        expected = Series(expected_data, dtype="object")
+        tm.assert_series_equal(result, expected)
+
+    [USER_REDACTED].mark.parametrize(
+        "dtype",
+        [
+            None,
+            "float64",
+            "int64",
+            "object",
+        ],
+    )
+    def test_round_empty_series(self, dtype):
+        # GH#63444
+        result = Series(dtype=dtype).round(4)
+        expected = Series(dtype=dtype)
+        tm.assert_series_equal(result, expected)
+
+    [USER_REDACTED].mark.parametrize(
+        "dtype,data",
+        [
+            ("string", ["a", "b", "c"]),
+            ("category", ["cat1", "cat2", "cat3"]),
+            (pd.StringDtype(), ["x", "y", "z"]),
+        ],
+    )
+    def test_round_non_numeric_dtype_noop(self, dtype, data):
+        # GH#63444, GH#63559
+        ser = Series(data, dtype=dtype)
+        result = ser.round(2)
+        tm.assert_series_equal(result, ser)

Thanks for the PR!

Looking good! Can you substantially reduce the opening comment on this PR (https://github.com/[USER_REDACTED]ndas-dev/pandas/pull/63463#issuecomment-3689836252)

Thanks for the changes [USER_REDACTED] - I think this is looking good! Taking a deeper look, it seems to me there are some edge cases we should think more about prior to raising (as I previously requested), some details below.

In addition, can you add an entry to the whatsnew 3.0 in the `Other enhancements` section. Something along the lines of

    - :meth:`Series.round` now supports object dtypes when the underlying Python objects implement ``__round__`` (:issue:`63444`)

lgtm

dtypes other than numeric and object should raise. Can you add a test for this case.

This and the comments below do not add anything that isn't already in the code. Can you remove them.

Can you parametrize this test.

Similar to the above - remove these comments.

Also move these tests to `tests/series` as the test below.

1. self.dtype != "object", you can use is_object_dtype
2. This will raise error for Non empty series with dtype str etc. which is working now
>>> pd.Series(["a", "b"], dtype='str').round()
0    a
1    b
dtype: str

do you need to copy?

there is already a map method, do you need map_infer?

do you need special handling of empty Series?

This request has not been addressed.

Agreed - we should use `is_object_dtype`.

For all added tests, can you add a reference to the issue as a comment on the first line.

```
# GH#63444
```

`is_object_dtype` is already imported; move the import of `is_numeric_dtype` to the top as well.

I've now twice unresolved this request as it still has not been actioned on without comment. Is there some question as to what it means?

Why are these being removed?

Docstrings should come before comments. But we generally do not put docstrings on tests.

This test duplicates other existing tests, please remove.

E.g. this should raise.

    pd.Series([], dtype=str).round()

Please restore this back to its original position.

This is still being removed and shouldn't be.

This is introducing `TestRound` when there is already `TestSeriesRound` in this file. I see nothing that differentiates these two. If that is indeed the case, please combine them as `TestSeriesRound`.

Please add a test for this case.

There are  a few edge cases I think we should be a bit careful of handling here, namely categorical with floating values. I've opened https://github.com/[USER_REDACTED]ndas-dev/pandas/issues/63559 to propose deprecating. Can you remove these two lines and adjust the tests so that it's checking `round` is a no-op (just returns the input Series).


DOC: Inline docstrings in strings/accessor.py (#62437)

Inlined docstrings for `str_pad` methods and `casemethods` in `pandas/core/strings/accessor.py` replacing `[USER_REDACTED]`. xef #62437.

@@ -149,18 +149,6 @@ def wrapper(self, *args, **kwargs):
     return _forbid_nonstring_types

-def _map_and_wrap(name: str | None, docstring: str | None):
-    [USER_REDACTED]_nonstring_types(["bytes"], name=name)
-    def wrapper(self):
-        result = getattr(self._data.array, f"_str_{name}")()
-        return self._wrap_result(
-            result, returns_string=name not in ("isnumeric", "isdecimal")
-        )
-
-    wrapper.__doc__ = docstring
-    return wrapper
-
-
 class StringMethods(NoNewAttributesMixin):
     """
     Vectorized string functions for Series and Index.
@@ -1813,78 +1801,190 @@ def pad(
         result = self._data.array._str_pad(width, side=side, fillchar=fillchar)
         return self._wrap_result(result)

-    _shared_docs["str_pad"] = """
-    Pad %(side)s side of strings in the Series/Index.
-
-    Equivalent to :meth:`str.%(method)s`.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def center(self, width: int, fillchar: str = " "):
+        """
+        Pad left and right side of strings in the Series/Index.

-    Parameters
-    ----------
-    width : int
-        Minimum width of resulting string; additional characters will be filled
-        with ``fillchar``.
-    fillchar : str
-        Additional character for filling, default is whitespace.
+        Equivalent to :meth:`str.center`.

-    Returns
-    -------
-    Series/Index of objects.
-        A Series or Index where the strings are modified by :meth:`str.%(method)s`.
+        Parameters
+        ----------
+        width : int
+            Minimum width of resulting string; additional characters will be filled
+            with ``fillchar``.
+        fillchar : str
+            Additional character for filling, default is whitespace.

-    [NAME_REDACTED]
-    --------
-    Series.str.rjust : Fills the left side of strings with an arbitrary
-        character.
-    Series.str.ljust : Fills the right side of strings with an arbitrary
-        character.
-    Series.str.center : Fills both sides of strings with an arbitrary
-        character.
-    Series.str.zfill : Pad strings in the Series/Index by prepending '0'
-        character.
+        Returns
+        -------
+        Series/Index of objects.
+            A Series or Index where the strings are modified by :meth:`str.center`.

-    Examples
-    --------
-    For Series.str.center:
+        See Also
+        --------
+        Series.str.rjust : Fills the left side of strings with an arbitrary
+            character.
+        Series.str.ljust : Fills the right side of strings with an arbitrary
+            character.
+        Series.str.center : Fills both sides of strings with an arbitrary
+            character.
+        Series.str.zfill : Pad strings in the Series/Index by prepending '0'
+            character.

-    >>> ser = pd.Series(['dog', 'bird', 'mouse'])
-    >>> ser.str.center(8, fillchar='.')
-    0   ..dog...
-    1   ..bird..
-    2   .mouse..
-    dtype: str
+        Examples
+        --------
+        For Series.str.center:

-    For Series.str.ljust:
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.center(8, fillchar=".")
+        0   ..dog...
+        1   ..bird..
+        2   .mouse..
+        dtype: str

-    >>> ser = pd.Series(['dog', 'bird', 'mouse'])
-    >>> ser.str.ljust(8, fillchar='.')
-    0   dog.....
-    1   bird....
-    2   mouse...
-    dtype: str
+        For Series.str.ljust:

-    For Series.str.rjust:
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.ljust(8, fillchar=".")
+        0   dog.....
+        1   bird....
+        2   mouse...
+        dtype: str

-    >>> ser = pd.Series(['dog', 'bird', 'mouse'])
-    >>> ser.str.rjust(8, fillchar='.')
-    0   .....dog
-    1   ....bird
-    2   ...mouse
-    dtype: str
-    """
+        For Series.str.rjust:

-    [USER_REDACTED](_shared_docs["str_pad"] % {"side": "left and right", "method": "center"})
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def center(self, width: int, fillchar: str = " "):
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.rjust(8, fillchar=".")
+        0   .....dog
+        1   ....bird
+        2   ...mouse
+        dtype: str
+        """
         return self.pad(width, side="both", fillchar=fillchar)

-    [USER_REDACTED](_shared_docs["str_pad"] % {"side": "right", "method": "ljust"})
     [USER_REDACTED]_nonstring_types(["bytes"])
     def ljust(self, width: int, fillchar: str = " "):
+        """
+        Pad right side of strings in the Series/Index.
+
+        Equivalent to :meth:`str.ljust`.
+
+        Parameters
+        ----------
+        width : int
+            Minimum width of resulting string; additional characters will be filled
+            with ``fillchar``.
+        fillchar : str
+            Additional character for filling, default is whitespace.
+
+        Returns
+        -------
+        Series/Index of objects.
+            A Series or Index where the strings are modified by :meth:`str.ljust`.
+
+        See Also
+        --------
+        Series.str.rjust : Fills the left side of strings with an arbitrary
+            character.
+        Series.str.ljust : Fills the right side of strings with an arbitrary
+            character.
+        Series.str.center : Fills both sides of strings with an arbitrary
+            character.
+        Series.str.zfill : Pad strings in the Series/Index by prepending '0'
+            character.
+
+        Examples
+        --------
+        For Series.str.center:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.center(8, fillchar=".")
+        0   ..dog...
+        1   ..bird..
+        2   .mouse..
+        dtype: str
+
+        For Series.str.ljust:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.ljust(8, fillchar=".")
+        0   dog.....
+        1   bird....
+        2   mouse...
+        dtype: str
+
+        For Series.str.rjust:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.rjust(8, fillchar=".")
+        0   .....dog
+        1   ....bird
+        2   ...mouse
+        dtype: str
+        """
         return self.pad(width, side="right", fillchar=fillchar)

-    [USER_REDACTED](_shared_docs["str_pad"] % {"side": "left", "method": "rjust"})
     [USER_REDACTED]_nonstring_types(["bytes"])
     def rjust(self, width: int, fillchar: str = " "):
+        """
+        Pad left side of strings in the Series/Index.
+
+        Equivalent to :meth:`str.rjust`.
+
+        Parameters
+        ----------
+        width : int
+            Minimum width of resulting string; additional characters will be filled
+            with ``fillchar``.
+        fillchar : str
+            Additional character for filling, default is whitespace.
+
+        Returns
+        -------
+        Series/Index of objects.
+            A Series or Index where the strings are modified by :meth:`str.rjust`.
+
+        See Also
+        --------
+        Series.str.rjust : Fills the left side of strings with an arbitrary
+            character.
+        Series.str.ljust : Fills the right side of strings with an arbitrary
+            character.
+        Series.str.center : Fills both sides of strings with an arbitrary
+            character.
+        Series.str.zfill : Pad strings in the Series/Index by prepending '0'
+            character.
+
+        Examples
+        --------
+        For Series.str.center:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.center(8, fillchar=".")
+        0   ..dog...
+        1   ..bird..
+        2   .mouse..
+        dtype: str
+
+        For Series.str.ljust:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.ljust(8, fillchar=".")
+        0   dog.....
+        1   bird....
+        2   mouse...
+        dtype: str
+
+        For Series.str.rjust:
+
+        >>> ser = pd.Series(["dog", "bird", "mouse"])
+        >>> ser.str.rjust(8, fillchar=".")
+        0   .....dog
+        1   ....bird
+        2   ...mouse
+        dtype: str
+        """
         return self.pad(width, side="left", fillchar=fillchar)

     [USER_REDACTED]_nonstring_types(["bytes"])
@@ -3359,503 +3459,896 @@ def len(self):
         result = self._data.array._str_len()
         return self._wrap_result(result, returns_string=False)

-    _shared_docs["casemethods"] = """
-    Convert strings in the Series/Index to %(type)s.
-    %(version)s
-    Equivalent to :meth:`str.%(method)s`.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def lower(self):
+        """
+        Convert strings in the Series/Index to lowercase.

-    Returns
-    -------
-    Series or Index of objects
-        A Series or Index where the strings are modified by :meth:`str.%(method)s`.
+        Equivalent to :meth:`str.lower`.

-    [NAME_REDACTED]
-    --------
-    Series.str.lower : Converts all characters to lowercase.
-    Series.str.upper : Converts all characters to uppercase.
-    Series.str.title : Converts first character of each word to uppercase and
-        remaining to lowercase.
-    Series.str.capitalize : Converts first character to uppercase and
-        remaining to lowercase.
-    Series.str.swapcase : Converts uppercase to lowercase and lowercase to
-        uppercase.
-    Series.str.casefold: Removes all case distinctions in the string.
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.lower`.

-    Examples
-    --------
-    >>> s = pd.Series(['lower', 'CAPITALS', 'this is a sentence', 'SwApCaSe'])
-    >>> s
-    0                 lower
-    1              CAPITALS
-    2    this is a sentence
-    3              SwApCaSe
-    dtype: str
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.

-    >>> s.str.lower()
-    0                 lower
-    1              capitals
-    2    this is a sentence
-    3              swapcase
-    dtype: str
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str

-    >>> s.str.upper()
-    0                 LOWER
-    1              CAPITALS
-    2    THIS IS A SENTENCE
-    3              SWAPCASE
-    dtype: str
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str

-    >>> s.str.title()
-    0                 Lower
-    1              Capitals
-    2    This Is A Sentence
-    3              Swapcase
-    dtype: str
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str

-    >>> s.str.capitalize()
-    0                 Lower
-    1              Capitals
-    2    This is a sentence
-    3              Swapcase
-    dtype: str
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str

-    >>> s.str.swapcase()
-    0                 LOWER
-    1              capitals
-    2    THIS IS A SENTENCE
-    3              sWaPcAsE
-    dtype: str
-    """
-    # Types:
-    #   cases:
-    #       upper, lower, title, capitalize, swapcase, casefold
-    #   boolean:
-    #     isalpha, isnumeric isalnum isdigit isdecimal isspace islower
-    #     isupper istitle isascii
-    # _doc_args holds dict of strings to use in substituting casemethod docs
-    _doc_args: dict[str, dict[str, str]] = {}
-    _doc_args["lower"] = {"type": "lowercase", "method": "lower", "version": ""}
-    _doc_args["upper"] = {"type": "uppercase", "method": "upper", "version": ""}
-    _doc_args["title"] = {"type": "titlecase", "method": "title", "version": ""}
-    _doc_args["capitalize"] = {
-        "type": "be capitalized",
-        "method": "capitalize",
-        "version": "",
-    }
-    _doc_args["swapcase"] = {
-        "type": "be swapcased",
-        "method": "swapcase",
-        "version": "",
-    }
-    _doc_args["casefold"] = {
-        "type": "be casefolded",
-        "method": "casefold",
-        "version": "",
-    }
-
-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["lower"])
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def lower(self):
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
         result = self._data.array._str_lower()
         return self._wrap_result(result)

-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["upper"])
     [USER_REDACTED]_nonstring_types(["bytes"])
     def upper(self):
-        result = self._data.array._str_upper()
-        return self._wrap_result(result)
+        """
+        Convert strings in the Series/Index to uppercase.

-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["title"])
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def title(self):
-        result = self._data.array._str_title()
-        return self._wrap_result(result)
+        Equivalent to :meth:`str.upper`.

-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["capitalize"])
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def capitalize(self):
-        result = self._data.array._str_capitalize()
-        return self._wrap_result(result)
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.upper`.

-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["swapcase"])
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def swapcase(self):
-        result = self._data.array._str_swapcase()
-        return self._wrap_result(result)
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.

-    [USER_REDACTED](_shared_docs["casemethods"] % _doc_args["casefold"])
-    [USER_REDACTED]_nonstring_types(["bytes"])
-    def casefold(self):
-        result = self._data.array._str_casefold()
-        return self._wrap_result(result)
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str

-    _shared_docs["ismethods"] = """
-    Check whether all characters in each string are %(type)s.
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str

-    This is equivalent to running the Python string method
-    :meth:`str.%(method)s` for each element of the Series/Index. If a string
-    has zero characters, ``False`` is returned for that check.
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str

-    Returns
-    -------
-    Series or Index of bool
-        Series or Index of boolean values with the same length as the original
-        Series/Index.
-    """
-    _shared_docs["isalpha"] = """
-    See Also
-    --------
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str

-    Examples
-    --------
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str

-    >>> s1 = pd.Series(['one', 'one1', '1', ''])
-    >>> s1.str.isalpha()
-    0     True
-    1    False
-    2    False
-    3    False
-    dtype: bool
-    """
-    _shared_docs["isnumeric"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
+        result = self._data.array._str_upper()
+        return self._wrap_result(result)

-    Examples
-    --------
-    The ``s.str.isnumeric`` method is the same as ``s3.str.isdigit`` but
-    also includes other characters that can represent quantities such as
-    unicode fractions.
-
-    >>> s1 = pd.Series(['one', 'one1', '1', '', '³', '⅕'])
-    >>> s1.str.isnumeric()
-    0    False
-    1    False
-    2     True
-    3    False
-    4     True
-    5     True
-    dtype: bool
-
-    For a string to be considered numeric, all its characters must have a Unicode
-    numeric property matching :py:meth:`str.is_numeric`. As a consequence,
-    the following cases are **not** recognized as numeric:
-
-    - **Decimal numbers** (e.g., "1.1"): due to period ``"."``
-    - **Negative numbers** (e.g., "-5"):  due to minus sign ``"-"``
-    - **Scientific notation** (e.g., "1e3"): due to characters like ``"e"``
-
-    >>> s2 = pd.Series(["1.1", "-5", "1e3"])
-    >>> s2.str.isnumeric()
-    0    False
-    1    False
-    2    False
-    dtype: bool
-    """
-    _shared_docs["isalnum"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def title(self):
+        """
+        Convert strings in the Series/Index to titlecase.

-    Examples
-    --------
-    >>> s1 = pd.Series(['one', 'one1', '1', ''])
-    >>> s1.str.isalnum()
-    0     True
-    1     True
-    2     True
-    3    False
-    dtype: bool
-
-    Note that checks against characters mixed with any additional punctuation
-    or whitespace will evaluate to false for an alphanumeric check.
-
-    >>> s2 = pd.Series(['A B', '1.5', '3,000'])
-    >>> s2.str.isalnum()
-    0    False
-    1    False
-    2    False
-    dtype: bool
-    """
-    _shared_docs["isdecimal"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        Equivalent to :meth:`str.title`.

-    Examples
-    --------
-    The ``s3.str.isdecimal`` method checks for characters used to form
-    numbers in base 10.
-
-    >>> s3 = pd.Series(['23', '³', '⅕', ''])
-    >>> s3.str.isdecimal()
-    0     True
-    1    False
-    2    False
-    3    False
-    dtype: bool
-    """
-    _shared_docs["isdigit"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.title`.

-    Notes
-    -----
-    Similar to ``str.isdecimal`` but also includes special digits, like
-    superscripted and subscripted digits in unicode.
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.

-    The exact behavior of this method, i.e. which unicode characters are
-    considered as digits, depends on the backend used for string operations,
-    and there can be small differences.
-    For example, Python considers the ³ superscript character as a digit, but
-    not the ⅕ fraction character, while PyArrow considers both as digits. For
-    simple (ascii) decimal numbers, the behaviour is consistent.
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str

-    Examples
-    --------
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str

-    >>> s3 = pd.Series(['23', '³', '⅕', ''])
-    >>> s3.str.isdigit()
-    0     True
-    1     True
-    2     True
-    3    False
-    dtype: bool
-    """
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str

-    _shared_docs["isspace"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str

-    Examples
-    --------
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str

-    >>> s4 = pd.Series([' ', '\\t\\r\\n ', ''])
-    >>> s4.str.isspace()
-    0     True
-    1     True
-    2    False
-    dtype: bool
-    """
-    _shared_docs["islower"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
-    Series.str.istitle : Check whether all characters are titlecase.
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
+        result = self._data.array._str_title()
+        return self._wrap_result(result)

-    Examples
-    --------
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def capitalize(self):
+        """
+        Convert strings in the Series/Index to be capitalized.

-    >>> s5 = pd.Series(['leopard', '[NAME_REDACTED]', 'SNAKE', ''])
-    >>> s5.str.islower()
-    0     True
-    1    False
-    2    False
-    3    False
-    dtype: bool
-    """
+        Equivalent to :meth:`str.capitalize`.

-    _shared_docs["isupper"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.istitle : Check whether all characters are titlecase.
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.capitalize`.

-    Examples
-    --------
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.

-    >>> s5 = pd.Series(['leopard', '[NAME_REDACTED]', 'SNAKE', ''])
-    >>> s5.str.isupper()
-    0    False
-    1    False
-    2     True
-    3    False
-    dtype: bool
-    """
-    _shared_docs["istitle"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.isascii : Check whether all characters are ascii.
-    Series.str.isupper : Check whether all characters are uppercase.
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str

-    Examples
-    --------
-    The ``s5.str.istitle`` method checks for whether all words are in title
-    case (whether only the first letter of each word is capitalized). Words are
-    assumed to be as any sequence of non-numeric characters separated by
-    whitespace characters.
-
-    >>> s5 = pd.Series(['leopard', '[NAME_REDACTED]', 'SNAKE', ''])
-    >>> s5.str.istitle()
-    0    False
-    1     True
-    2    False
-    3    False
-    dtype: bool
-    """
-    _shared_docs["isascii"] = """
-    See Also
-    --------
-    Series.str.isalpha : Check whether all characters are alphabetic.
-    Series.str.isnumeric : Check whether all characters are numeric.
-    Series.str.isalnum : Check whether all characters are alphanumeric.
-    Series.str.isdigit : Check whether all characters are digits.
-    Series.str.isdecimal : Check whether all characters are decimal.
-    Series.str.isspace : Check whether all characters are whitespace.
-    Series.str.islower : Check whether all characters are lowercase.
-    Series.str.istitle : Check whether all characters are titlecase.
-    Series.str.isupper : Check whether all characters are uppercase.
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str

-    Examples
-    --------
-    The ``s5.str.isascii`` method checks for whether all characters are ascii
-    characters, which includes digits 0-9, capital and lowercase letters A-Z,
-    and some other special characters.
-
-    >>> s5 = pd.Series(['ö', 'see123', 'hello world', ''])
-    >>> s5.str.isascii()
-    0    False
-    1     True
-    2     True
-    3     True
-    dtype: bool
-    """
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str

-    _doc_args["isalnum"] = {"type": "alphanumeric", "method": "isalnum"}
-    _doc_args["isalpha"] = {"type": "alphabetic", "method": "isalpha"}
-    _doc_args["isdigit"] = {"type": "digits", "method": "isdigit"}
-    _doc_args["isspace"] = {"type": "whitespace", "method": "isspace"}
-    _doc_args["islower"] = {"type": "lowercase", "method": "islower"}
-    _doc_args["isascii"] = {"type": "ascii", "method": "isascii"}
-    _doc_args["isupper"] = {"type": "uppercase", "method": "isupper"}
-    _doc_args["istitle"] = {"type": "titlecase", "method": "istitle"}
-    _doc_args["isnumeric"] = {"type": "numeric", "method": "isnumeric"}
-    _doc_args["isdecimal"] = {"type": "decimal", "method": "isdecimal"}
-    # force _noarg_wrapper return type with dtype=np.dtype(bool) (GH 29624)
-
-    isalnum = _map_and_wrap(
-        "isalnum",
-        docstring=_shared_docs["ismethods"] % _doc_args["isalnum"]
-        + _shared_docs["isalnum"],
-    )
-    isalpha = _map_and_wrap(
-        "isalpha",
-        docstring=_shared_docs["ismethods"] % _doc_args["isalpha"]
-        + _shared_docs["isalpha"],
-    )
-    isdigit = _map_and_wrap(
-        "isdigit",
-        docstring=_shared_docs["ismethods"] % _doc_args["isdigit"]
-        + _shared_docs["isdigit"],
-    )
-    isspace = _map_and_wrap(
-        "isspace",
-        docstring=_shared_docs["ismethods"] % _doc_args["isspace"]
-        + _shared_docs["isspace"],
-    )
-    islower = _map_and_wrap(
-        "islower",
-        docstring=_shared_docs["ismethods"] % _doc_args["islower"]
-        + _shared_docs["islower"],
-    )
-    isascii = _map_and_wrap(
-        "isascii",
-        docstring=_shared_docs["ismethods"] % _doc_args["isascii"]
-        + _shared_docs["isascii"],
-    )
-    isupper = _map_and_wrap(
-        "isupper",
-        docstring=_shared_docs["ismethods"] % _doc_args["isupper"]
-        + _shared_docs["isupper"],
-    )
-    istitle = _map_and_wrap(
-        "istitle",
-        docstring=_shared_docs["ismethods"] % _doc_args["istitle"]
-        + _shared_docs["istitle"],
-    )
-    isnumeric = _map_and_wrap(
-        "isnumeric",
-        docstring=_shared_docs["ismethods"] % _doc_args["isnumeric"]
-        + _shared_docs["isnumeric"],
-    )
-    isdecimal = _map_and_wrap(
-        "isdecimal",
-        docstring=_shared_docs["ismethods"] % _doc_args["isdecimal"]
-        + _shared_docs["isdecimal"],
-    )
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
+        result = self._data.array._str_capitalize()
+        return self._wrap_result(result)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def swapcase(self):
+        """
+        Convert strings in the Series/Index to be swapcased.
+
+        Equivalent to :meth:`str.swapcase`.
+
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.swapcase`.
+
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.
+
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str
+
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str
+
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str
+
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
+        result = self._data.array._str_swapcase()
+        return self._wrap_result(result)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def casefold(self):
+        """
+        Convert strings in the Series/Index to be casefolded.
+
+        Equivalent to :meth:`str.casefold`.
+
+        Returns
+        -------
+        Series or Index of objects
+            A Series or Index where the strings are modified by :meth:`str.casefold`.
+
+        See Also
+        --------
+        Series.str.lower : Converts all characters to lowercase.
+        Series.str.upper : Converts all characters to uppercase.
+        Series.str.title : Converts first character of each word to uppercase and
+            remaining to lowercase.
+        Series.str.capitalize : Converts first character to uppercase and
+            remaining to lowercase.
+        Series.str.swapcase : Converts uppercase to lowercase and lowercase to
+            uppercase.
+        Series.str.casefold: Removes all case distinctions in the string.
+
+        Examples
+        --------
+        >>> s = pd.Series(["lower", "CAPITALS", "this is a sentence", "SwApCaSe"])
+        >>> s
+        0                 lower
+        1              CAPITALS
+        2    this is a sentence
+        3              SwApCaSe
+        dtype: str
+
+        >>> s.str.lower()
+        0                 lower
+        1              capitals
+        2    this is a sentence
+        3              swapcase
+        dtype: str
+
+        >>> s.str.upper()
+        0                 LOWER
+        1              CAPITALS
+        2    THIS IS A SENTENCE
+        3              SWAPCASE
+        dtype: str
+
+        >>> s.str.title()
+        0                 Lower
+        1              Capitals
+        2    This Is A Sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.capitalize()
+        0                 Lower
+        1              Capitals
+        2    This is a sentence
+        3              Swapcase
+        dtype: str
+
+        >>> s.str.swapcase()
+        0                 LOWER
+        1              capitals
+        2    THIS IS A SENTENCE
+        3              sWaPcAsE
+        dtype: str
+        """
+        result = self._data.array._str_casefold()
+        return self._wrap_result(result)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isalnum(self):
+        """
+        Check whether all characters in each string are alphanumeric.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isalnum` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+        >>> s1 = pd.Series(["one", "one1", "1", ""])
+        >>> s1.str.isalnum()
+        0     True
+        1     True
+        2     True
+        3    False
+        dtype: bool
+
+        Note that checks against characters mixed with any additional punctuation
+        or whitespace will evaluate to false for an alphanumeric check.
+
+        >>> s2 = pd.Series(["A B", "1.5", "3,000"])
+        >>> s2.str.isalnum()
+        0    False
+        1    False
+        2    False
+        dtype: bool
+        """
+        result = self._data.array._str_isalnum()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isalpha(self):
+        """
+        Check whether all characters in each string are alphabetic.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isalpha` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+
+        >>> s1 = pd.Series(["one", "one1", "1", ""])
+        >>> s1.str.isalpha()
+        0     True
+        1    False
+        2    False
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_isalpha()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isdigit(self):
+        """
+        Check whether all characters in each string are digits.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isdigit` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Notes
+        -----
+        Similar to ``str.isdecimal`` but also includes special digits, like
+        superscripted and subscripted digits in unicode.
+
+        The exact behavior of this method, i.e. which unicode characters are
+        considered as digits, depends on the backend used for string operations,
+        and there can be small differences.
+        For example, Python considers the ³ superscript character as a digit, but
+        not the ⅕ fraction character, while PyArrow considers both as digits. For
+        simple (ascii) decimal numbers, the behaviour is consistent.
+
+        Examples
+        --------
+
+        >>> s3 = pd.Series(["23", "³", "⅕", ""])
+        >>> s3.str.isdigit()
+        0     True
+        1     True
+        2     True
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_isdigit()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isspace(self):
+        """
+        Check whether all characters in each string are whitespace.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isspace` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+
+        >>> s4 = pd.Series([" ", "\\t\\r\\n ", ""])
+        >>> s4.str.isspace()
+        0     True
+        1     True
+        2    False
+        dtype: bool
+        """
+        result = self._data.array._str_isspace()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def islower(self):
+        """
+        Check whether all characters in each string are lowercase.
+
+        This is equivalent to running the Python string method
+        :meth:`str.islower` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+
+        >>> s5 = pd.Series(["leopard", "[NAME_REDACTED]", "SNAKE", ""])
+        >>> s5.str.islower()
+        0     True
+        1    False
+        2    False
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_islower()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isascii(self):
+        """
+        Check whether all characters in each string are ascii.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isascii` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+        The ``s5.str.isascii`` method checks for whether all characters are ascii
+        characters, which includes digits 0-9, capital and lowercase letters A-Z,
+        and some other special characters.
+
+        >>> s5 = pd.Series(["ö", "see123", "hello world", ""])
+        >>> s5.str.isascii()
+        0    False
+        1     True
+        2     True
+        3     True
+        dtype: bool
+        """
+        result = self._data.array._str_isascii()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isupper(self):
+        """
+        Check whether all characters in each string are uppercase.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isupper` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+
+        >>> s5 = pd.Series(["leopard", "[NAME_REDACTED]", "SNAKE", ""])
+        >>> s5.str.isupper()
+        0    False
+        1    False
+        2     True
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_isupper()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def istitle(self):
+        """
+        Check whether all characters in each string are titlecase.
+
+        This is equivalent to running the Python string method
+        :meth:`str.istitle` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+
+        Examples
+        --------
+        The ``s5.str.istitle`` method checks for whether all words are in title
+        case (whether only the first letter of each word is capitalized). Words are
+        assumed to be as any sequence of non-numeric characters separated by
+        whitespace characters.
+
+        >>> s5 = pd.Series(["leopard", "[NAME_REDACTED]", "SNAKE", ""])
+        >>> s5.str.istitle()
+        0    False
+        1     True
+        2    False
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_istitle()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isnumeric(self):
+        """
+        Check whether all characters in each string are numeric.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isnumeric` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isdecimal : Check whether all characters are decimal.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+        The ``s.str.isnumeric`` method is the same as ``s3.str.isdigit`` but
+        also includes other characters that can represent quantities such as
+        unicode fractions.
+
+        >>> s1 = pd.Series(["one", "one1", "1", "", "³", "⅕"])
+        >>> s1.str.isnumeric()
+        0    False
+        1    False
+        2     True
+        3    False
+        4     True
+        5     True
+        dtype: bool
+
+        For a string to be considered numeric, all its characters must have a Unicode
+        numeric property matching :py:meth:`str.is_numeric`. As a consequence,
+        the following cases are **not** recognized as numeric:
+
+        - **Decimal numbers** (e.g., "1.1"): due to period ``"."``
+        - **Negative numbers** (e.g., "-5"):  due to minus sign ``"-"``
+        - **Scientific notation** (e.g., "1e3"): due to characters like ``"e"``
+
+        >>> s2 = pd.Series(["1.1", "-5", "1e3"])
+        >>> s2.str.isnumeric()
+        0    False
+        1    False
+        2    False
+        dtype: bool
+        """
+        result = self._data.array._str_isnumeric()
+        return self._wrap_result(result, returns_string=False)
+
+    [USER_REDACTED]_nonstring_types(["bytes"])
+    def isdecimal(self):
+        """
+        Check whether all characters in each string are decimal.
+
+        This is equivalent to running the Python string method
+        :meth:`str.isdecimal` for each element of the Series/Index. If a string
+        has zero characters, ``False`` is returned for that check.
+
+        Returns
+        -------
+        Series or Index of bool
+            Series or Index of boolean values with the same length as the original
+            Series/Index.
+
+        See Also
+        --------
+        Series.str.isalpha : Check whether all characters are alphabetic.
+        Series.str.isnumeric : Check whether all characters are numeric.
+        Series.str.isalnum : Check whether all characters are alphanumeric.
+        Series.str.isdigit : Check whether all characters are digits.
+        Series.str.isspace : Check whether all characters are whitespace.
+        Series.str.islower : Check whether all characters are lowercase.
+        Series.str.isascii : Check whether all characters are ascii.
+        Series.str.isupper : Check whether all characters are uppercase.
+        Series.str.istitle : Check whether all characters are titlecase.
+
+        Examples
+        --------
+        The ``s3.str.isdecimal`` method checks for characters used to form
+        numbers in base 10.
+
+        >>> s3 = pd.Series(["23", "³", "⅕", ""])
+        >>> s3.str.isdecimal()
+        0     True
+        1    False
+        2    False
+        3    False
+        dtype: bool
+        """
+        result = self._data.array._str_isdecimal()
+        return self._wrap_result(result, returns_string=False)

 def cat_safe(list_of_columns: list[npt.NDArray[np.object_]], sep: str):

Is `_doc_args` used anywhere anymore? If not, could you remove this block of code?