Repository: pandas-dev/pandas (python)

Issue:
DOC: Clarify to_numeric behavior for numeric dtypes
### Pandas version checks

- [x] I have checked that the issue still exists on the latest versions of the docs on `main` [here](https://pandas.pydata.org/docs/dev/)


### Location of the documentation

https://pandas.pydata.org/docs/dev/reference/api/pandas.to_numeric.html#pandas-to-numeric

### Documentation problem

The docstring for the `to_numeric` function needs to be improved for clarity and accuracy. The current documentation states, "The default return dtype is float64 or int64," which can be misleading. This statement doesn't account for cases where the input data is already of a numeric ExtensionDtype (e.g., Int32, Float32, or Arrow dtypes where `_is_numeric` is `True`). In these instances, `to_numeric` correctly preserves the original dtype rather than converting it, making the current documentation incomplete.

### Suggested fix for documentation

1. If the input is already of a numeric dtype, its dtype is preserved.
2. The conversion to a default float64 or int64 dtype primarily applies to non-numeric inputs.

Pull request:
DOC: Clarify to_numeric behavior for numeric dtypes
- [x] closes #61903 
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 1383202154..a096e977e3 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -457,6 +457,7 @@ Other Deprecations
 - Deprecated :meth:`.DataFrameGroupby.corrwith` (:issue:`57158`)
 - Deprecated :meth:`Timestamp.utcfromtimestamp`, use ``Timestamp.fromtimestamp(ts, "UTC")`` instead (:issue:`56680`)
 - Deprecated :meth:`Timestamp.utcnow`, use ``Timestamp.now("UTC")`` instead (:issue:`56680`)
+- Deprecated ``pd.core.internals.api.maybe_infer_ndim`` (:issue:`40226`)
 - Deprecated allowing non-keyword arguments in :meth:`DataFrame.all`, :meth:`DataFrame.min`, :meth:`DataFrame.max`, :meth:`DataFrame.sum`, :meth:`DataFrame.prod`, :meth:`DataFrame.mean`, :meth:`DataFrame.median`, :meth:`DataFrame.sem`, :meth:`DataFrame.var`, :meth:`DataFrame.std`, :meth:`DataFrame.skew`, :meth:`DataFrame.kurt`, :meth:`Series.all`,  :meth:`Series.min`, :meth:`Series.max`, :meth:`Series.sum`, :meth:`Series.prod`, :meth:`Series.mean`, :meth:`Series.median`, :meth:`Series.sem`, :meth:`Series.var`, :meth:`Series.std`, :meth:`Series.skew`, and :meth:`Series.kurt`. (:issue:`57087`)
 - Deprecated allowing non-keyword arguments in :meth:`Series.to_markdown` except ``buf``. (:issue:`57280`)
 - Deprecated allowing non-keyword arguments in :meth:`Series.to_string` except ``buf``. (:issue:`57280`)
diff --git a/pandas/_libs/internals.pyx b/pandas/_libs/internals.pyx
index 8caf7e5fb9..4fb24c9ad1 100644
--- a/pandas/_libs/internals.pyx
+++ b/pandas/_libs/internals.pyx
@@ -709,9 +709,9 @@ cdef class Block:
             self.ndim = state[2]
         else:
             # older pickle
-            from pandas.core.internals.api import maybe_infer_ndim
+            from pandas.core.internals.api import _maybe_infer_ndim
 
-            ndim = maybe_infer_ndim(self.values, self.mgr_locs)
+            ndim = _maybe_infer_ndim(self.values, self.mgr_locs)
             self.ndim = ndim
 
     cpdef Block slice_block_rows(self, slice slicer):
diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py
index 50fecc96f8..d5e654c955 100644
--- a/pandas/core/arrays/datetimelike.py
+++ b/pandas/core/arrays/datetimelike.py
@@ -1778,7 +1778,7 @@ class DatelikeOps(DatetimeLikeArrayMixin):
         >>> rng.strftime("%%B %%d, %%Y, %%r")
         Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM',
                'March 10, 2018, 09:00:02 AM'],
-              dtype='object')
+              dtype='str')
         """
         result = self._format_native_types(date_format=date_format, na_rep=np.nan)
         if using_string_dtype():
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
index b31c543188..57c138d982 100644
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -1308,14 +1308,14 @@ default 'raise'
         0     January
         1    February
         2       March
-        dtype: object
+        dtype: str
 
         >>> idx = pd.date_range(start="2018-01", freq="ME", periods=3)
         >>> idx
         DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31'],
                       dtype='datetime64[ns]', freq='ME')
         >>> idx.month_name()
-        Index(['January', 'February', 'March'], dtype='object')
+        Index(['January', 'February', 'March'], dtype='str')
 
         Using the ``locale`` parameter you can set a different locale language,
         for example: ``idx.month_name(locale='pt_BR.utf8')`` will return month
@@ -1326,7 +1326,7 @@ default 'raise'
         DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31'],
                       dtype='datetime64[ns]', freq='ME')
         >>> idx.month_name(locale="pt_BR.utf8")  # doctest: +SKIP
-        Index(['Janeiro', 'Fevereiro', 'Março'], dtype='object')
+        Index(['Janeiro', 'Fevereiro', 'Março'], dtype='str')
         """
         values = self._local_timestamps()
 
@@ -1376,14 +1376,14 @@ default 'raise'
         0       Monday
         1      Tuesday
         2    Wednesday
-        dtype: object
+        dtype: str
 
         >>> idx = pd.date_range(start="2018-01-01", freq="D", periods=3)
         >>> idx
         DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03'],
                       dtype='datetime64[ns]', freq='D')
         >>> idx.day_name()
-        Index(['Monday', 'Tuesday', 'Wednesday'], dtype='object')
+        Index(['Monday', 'Tuesday', 'Wednesday'], dtype='str')
 
         Using the ``locale`` parameter you can set a different locale language,
         for example: ``idx.day_name(locale='pt_BR.utf8')`` will return day
@@ -1394,7 +1394,7 @@ default 'raise'
         DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03'],
                       dtype='datetime64[ns]', freq='D')
         >>> idx.day_name(locale="pt_BR.utf8")  # doctest: +SKIP
-        Index(['Segunda', 'Terça', 'Quarta'], dtype='object')
+        Index(['Segunda', 'Terça', 'Quarta'], dtype='str')
         """
         values = self._local_timestamps()
 
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 6424589843..7b70ac3588 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -1918,7 +1918,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         b  2  4
         c  3  8
         >>> d.keys()
-        Index(['A', 'B'], dtype='object')
+        Index(['A', 'B'], dtype='str')
         """
         return self._info_axis
 
@@ -6276,7 +6276,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         float              float64
         int                  int64
         datetime    datetime64[s]
-        string              object
+        string              str
         dtype: object
         """
         data = self._mgr.get_dtypes()
@@ -6838,7 +6838,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         0      a
         1      b
         2    NaN
-        dtype: object
+        dtype: str
 
         Obtain a Series with dtype ``StringDtype``.
 
@@ -8138,7 +8138,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         ... )
         >>> df
            age       born    name        toy
-        0  5.0        NaT  Alfred       None
+        0  5.0        NaT  Alfred        NaN
         1  6.0 1939-05-27  Batman  Batmobile
         2  NaN 1940-04-25              Joker
 
@@ -8211,7 +8211,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         ... )
         >>> df
            age       born    name        toy
-        0  5.0        NaT  Alfred       None
+        0  5.0        NaT  Alfred        NaN
         1  6.0 1939-05-27  Batman  Batmobile
         2  NaN 1940-04-25              Joker
 
@@ -10401,7 +10401,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         2    b
         3    c
         4    d
-        Name: A, dtype: object
+        Name: A, dtype: str
 
         The index values in ``truncate`` can be datetimes or string
         dates.
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index fef0c68d90..e8c5a03a6d 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -1209,7 +1209,7 @@ class Index(IndexOpsMixin, PandasObject):
         --------
         >>> idx = pd.Index(['a', 'b', 'c'])
         >>> idx.take([2, 2, 1, 2])
-        Index(['c', 'c', 'b', 'c'], dtype='object')
+        Index(['c', 'c', 'b', 'c'], dtype='str')
         """
 
     @Appender(_index_shared_docs["take"] % _index_doc_kwargs)
@@ -1415,7 +1415,7 @@ class Index(IndexOpsMixin, PandasObject):
             is_justify = False
         elif isinstance(self.dtype, CategoricalDtype):
             self = cast("CategoricalIndex", self)
-            if is_object_dtype(self.categories.dtype):
+            if is_string_dtype(self.categories.dtype):
                 is_justify = False
         elif isinstance(self, ABCRangeIndex):
             # We will do the relevant formatting via attrs
@@ -6862,11 +6862,11 @@ class Index(IndexOpsMixin, PandasObject):
         --------
         >>> idx = pd.Index(["a", "b", "c"])
         >>> idx.delete(1)
-        Index(['a', 'c'], dtype='object')
+        Index(['a', 'c'], dtype='str')
 
         >>> idx = pd.Index(["a", "b", "c"])
         >>> idx.delete([0, 2])
-        Index(['b'], dtype='object')
+        Index(['b'], dtype='str')
         """
         values = self._values
         res_values: ArrayLike
@@ -6906,7 +6906,7 @@ class Index(IndexOpsMixin, PandasObject):
         --------
         >>> idx = pd.Index(["a", "b", "c"])
         >>> idx.insert(1, "x")
-        Index(['a', 'x', 'b', 'c'], dtype='object')
+        Index(['a', 'x', 'b', 'c'], dtype='str')
         """
         item = lib.item_from_zerodim(item)
         if is_valid_na_for_dtype(item, self.dtype) and self.dtype != object:
diff --git a/pandas/core/internals/api.py b/pandas/core/internals/api.py
index c5d6a2fe7a..fe2af4e142 100644
--- a/pandas/core/internals/api.py
+++ b/pandas/core/internals/api.py
@@ -136,7 +136,7 @@ def make_block(
     if not isinstance(placement, BlockPlacement):
         placement = BlockPlacement(placement)
 
-    ndim = maybe_infer_ndim(values, placement, ndim)
+    ndim = _maybe_infer_ndim(values, placement, ndim)
     if isinstance(values.dtype, (PeriodDtype, DatetimeTZDtype)):
         # GH#41168 ensure we can pass 1D dt64tz values
         # More generally, any EA dtype that isn't is_1d_only_ea_dtype
@@ -148,7 +148,7 @@ def make_block(
     return klass(values, ndim=ndim, placement=placement)
 
 
-def maybe_infer_ndim(values, placement: BlockPlacement, ndim: int | None) -> int:
+def _maybe_infer_ndim(values, placement: BlockPlacement, ndim: int | None) -> int:
     """
     If `ndim` is not provided, infer it from placement and values.
     """
@@ -162,3 +162,15 @@ def maybe_infer_ndim(values, placement: BlockPlacement, ndim: int | None) -> int
         else:
             ndim = values.ndim
     return ndim
+
+
+def maybe_infer_ndim(values, placement: BlockPlacement, ndim: int | None) -> int:
+    """
+    If `ndim` is not provided, infer it from placement and values.
+    """
+    warnings.warn(
+        "maybe_infer_ndim is deprecated and will be removed in a future version.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return _maybe_infer_ndim(values, placement, ndim)
diff --git a/pandas/core/tools/numeric.py b/pandas/core/tools/numeric.py
index c2ffe17adb..54f4be1cb8 100644
--- a/pandas/core/tools/numeric.py
+++ b/pandas/core/tools/numeric.py
@@ -55,7 +55,8 @@ def to_numeric(
     """
     Convert argument to a numeric type.
 
-    The default return dtype is `float64` or `int64`
+    If the input is already of a numeric dtype, the dtype will be preserved.
+    For non-numeric inputs, the default return dtype is `float64` or `int64`
     depending on the data supplied. Use the `downcast` parameter
     to obtain other dtypes.
 


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index d2899e3838..412f27cba9 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -91,7 +91,7 @@ jobs:
       LANG: ${{ matrix.lang || 'C.UTF-8' }}
       LC_ALL: ${{ matrix.lc_all || '' }}
       PANDAS_CI: '1'
-      PANDAS_FUTURE_INFER_STRING: ${{ matrix.pandas_future_infer_string || '0' }}
+      PANDAS_FUTURE_INFER_STRING: ${{ matrix.pandas_future_infer_string || '1' }}
       TEST_ARGS: ${{ matrix.test_args || '' }}
       PYTEST_WORKERS: 'auto'
       PYTEST_TARGET: ${{ matrix.pytest_target || 'pandas' }}
diff --git a/pandas/tests/indexes/categorical/test_formats.py b/pandas/tests/indexes/categorical/test_formats.py
index b100740b06..2308a62bc4 100644
--- a/pandas/tests/indexes/categorical/test_formats.py
+++ b/pandas/tests/indexes/categorical/test_formats.py
@@ -2,9 +2,6 @@
 Tests for CategoricalIndex.__repr__ and related methods.
 """
 
-import pytest
-
-from pandas._config import using_string_dtype
 import pandas._config.config as cf
 
 from pandas import CategoricalIndex
@@ -22,7 +19,6 @@ class TestCategoricalIndexReprStringCategories:
             )
         assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_multiline(self, using_infer_string):
         # multiple lines
         idx = CategoricalIndex(["a", "bb", "ccc"] * 10)
@@ -37,7 +33,6 @@ class TestCategoricalIndexReprStringCategories:
             )
         assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_truncated(self, using_infer_string):
         # truncated
         idx = CategoricalIndex(["a", "bb", "ccc"] * 100)
@@ -76,7 +71,6 @@ class TestCategoricalIndexReprStringCategories:
             )
         assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_unicode_multiline(self, using_infer_string):
         # multiple lines
         idx = CategoricalIndex(["あ", "いい", "ううう"] * 10)
@@ -91,7 +85,6 @@ class TestCategoricalIndexReprStringCategories:
             )
         assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_unicode_truncated(self, using_infer_string):
         # truncated
         idx = CategoricalIndex(["あ", "いい", "ううう"] * 100)
@@ -131,7 +124,6 @@ class TestCategoricalIndexReprStringCategories:
                 )
             assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_east_asian_width_multiline(
         self, using_infer_string
     ):
@@ -151,7 +143,6 @@ class TestCategoricalIndexReprStringCategories:
                 )
             assert repr(idx) == expected
 
-    @pytest.mark.xfail(using_string_dtype(), reason="Different padding on multi-line")
     def test_categorical_index_repr_east_asian_width_truncated(
         self, using_infer_string
     ):
diff --git a/pandas/tests/indexing/test_iloc.py b/pandas/tests/indexing/test_iloc.py
index 3be69617ca..5ea1113310 100644
--- a/pandas/tests/indexing/test_iloc.py
+++ b/pandas/tests/indexing/test_iloc.py
@@ -6,6 +6,7 @@ import re
 import numpy as np
 import pytest
 
+from pandas.compat import pa_version_under16p0
 from pandas.errors import IndexingError
 
 from pandas import (
@@ -140,6 +141,9 @@ class TestiLocBaseIndependent:
         df = ser.to_frame()
         assert df.iloc._is_scalar_access((1, 0))
 
+    @pytest.mark.skipif(
+        pa_version_under16p0, reason="https://github.com/apache/arrow/issues/40642"
+    )
     def test_iloc_exceeds_bounds(self):
         # GH6296
         # iloc should allow indexers that exceed the bounds
diff --git a/pandas/tests/internals/test_api.py b/pandas/tests/internals/test_api.py
index fc222f6987..f38bed42a3 100644
--- a/pandas/tests/internals/test_api.py
+++ b/pandas/tests/internals/test_api.py
@@ -79,6 +79,15 @@ def test_create_block_manager_from_blocks_deprecated():
         internals.create_block_manager_from_blocks
 
 
+def test_maybe_infer_ndim_deprecated():
+    # GH#40226
+    msg = "maybe_infer_ndim is deprecated and will be removed in a future version."
+    arr = np.arange(5)
+    bp = pd._libs.internals.BlockPlacement([1])
+    with tm.assert_produces_warning(DeprecationWarning, match=msg):
+        internals.api.maybe_infer_ndim(arr, bp, 1)
+
+
 def test_create_dataframe_from_blocks(float_frame):
     block = float_frame._mgr.blocks[0]
     index = float_frame.index.copy()
diff --git a/pandas/tests/io/test_gcs.py b/pandas/tests/io/test_gcs.py
index f68ef5fa2e..78c612472c 100644
--- a/pandas/tests/io/test_gcs.py
+++ b/pandas/tests/io/test_gcs.py
@@ -216,7 +216,6 @@ def test_to_parquet_gcs_new_file(monkeypatch, tmpdir):
         {
             "int": [1, 3],
             "float": [2.0, np.nan],
-            "str": ["t", "s"],
             "dt": date_range("2018-06-18", periods=2),
         }
     )