Repository: pandas-dev/pandas
Pull request #61722 (2025-06-27 13:43:03)

String dtype: turn on by default
Now that 2.3.0 is released, time to switch the default on main to prepare for a 3.0 release (and also such that people using nightlies get this)

I assume this might also still uncover some failing tests. And the docstrings will still fail for sure.

jorisvandenbossche: Aside docstrings, the only tests that were failing are the ones with the minimum pyarrow dependency of 10.0.1. For our support window of 2 years, we can bump that to 12.0.1. If that helps, will move it to a separate PR.
mroeschke: > Shall we do that here?

I would like to keep the option for users to switch back to the old string type if needed 
jbrockmendel: looks like 20 tests (76 with parametrization) are xfailed.  At least some of these will be easy-but-tedious of updating `expected`s that I'll get out of the way now.  I don't see any reason why these would be a blocker for this PR.

Patch:
diff --git a/.github/workflows/docbuild-and-upload.yml b/.github/workflows/docbuild-and-upload.yml
index ba9e30e088..e982b18ec7 100644
--- a/.github/workflows/docbuild-and-upload.yml
+++ b/.github/workflows/docbuild-and-upload.yml
@@ -57,6 +57,8 @@ jobs:
       run: python web/pandas_web.py web/pandas --target-path=web/build
 
     - name: Build documentation
+      # TEMP don't let errors fail the build until all string dtype changes are fixed
+      continue-on-error: true
       run: doc/make.py --warnings-are-errors
 
     - name: Build the interactive terminal
diff --git a/ci/code_checks.sh b/ci/code_checks.sh
index a0d23aa047..a310b71d59 100755
--- a/ci/code_checks.sh
+++ b/ci/code_checks.sh
@@ -58,7 +58,9 @@ if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then
 
     MSG='Python and Cython Doctests' ; echo "$MSG"
     python -c 'import pandas as pd; pd.test(run_doctests=True)'
-    RET=$(($RET + $?)) ; echo "$MSG" "DONE"
+    # TEMP don't let doctests fail the build until all string dtype changes are fixed
+    # RET=$(($RET + $?)) ; echo "$MSG" "DONE"
+    echo "$MSG" "DONE"
 
 fi
 
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 977186d808..f2650a64d2 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -414,6 +414,7 @@ Other API changes
 - Index set operations (like union or intersection) will now ignore the dtype of
   an empty ``RangeIndex`` or empty ``Index`` with object dtype when determining
   the dtype of the resulting Index (:issue:`60797`)
+- Numpy functions like ``np.isinf`` that return a bool dtype when called on a :class:`Index` object now return a bool-dtype :class:`Index` instead of ``np.ndarray`` (:issue:`52676`)
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_300.deprecations:
diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py
index 20fe8cbab1..bf7e8fb02b 100644
--- a/pandas/core/config_init.py
+++ b/pandas/core/config_init.py
@@ -880,7 +880,7 @@ with cf.config_prefix("styler"):
 with cf.config_prefix("future"):
     cf.register_option(
         "infer_string",
-        True if os.environ.get("PANDAS_FUTURE_INFER_STRING", "0") == "1" else False,
+        False if os.environ.get("PANDAS_FUTURE_INFER_STRING", "1") == "0" else True,
         "Whether to infer sequence of str objects as pyarrow string "
         "dtype, which will be the default in pandas 3.0 "
         "(at which point this option will be deprecated).",
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 0b719ae21d..fb395f4f7b 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -965,12 +965,8 @@ class Index(IndexOpsMixin, PandasObject):
         Gets called after a ufunc and other functions e.g. np.split.
         """
         result = lib.item_from_zerodim(result)
-        if (not isinstance(result, Index) and is_bool_dtype(result.dtype)) or np.ndim(
-            result
-        ) > 1:
-            # exclude Index to avoid warning from is_bool_dtype deprecation;
-            #  in the Index case it doesn't matter which path we go down.
-            # reached in plotting tests with e.g. np.nonzero(index)
+        if np.ndim(result) > 1:
+            # Reached in plotting tests with e.g. np.nonzero(index)
             return result
 
         return Index(result, name=self.name)
diff --git a/pandas/core/reshape/encoding.py b/pandas/core/reshape/encoding.py
index ad4a5db441..67fb075110 100644
--- a/pandas/core/reshape/encoding.py
+++ b/pandas/core/reshape/encoding.py
@@ -390,7 +390,9 @@ def from_dummies(
         The default category is the implied category when a value has none of the
         listed categories specified with a one, i.e. if all dummies in a row are
         zero. Can be a single value for all variables or a dict directly mapping
-        the default categories to a prefix of a variable.
+        the default categories to a prefix of a variable. The default category
+        will be coerced to the dtype of ``data.columns`` if such coercion is
+        lossless, and will raise otherwise.
 
     Returns
     -------
diff --git a/pandas/core/tools/numeric.py b/pandas/core/tools/numeric.py
index bc45343d6e..c2ffe17adb 100644
--- a/pandas/core/tools/numeric.py
+++ b/pandas/core/tools/numeric.py
@@ -114,6 +114,14 @@ def to_numeric(
         Numeric if parsing succeeded.
         Return type depends on input.  Series if Series, otherwise ndarray.
 
+    Raises
+    ------
+    ValueError
+        If the input contains non-numeric values and `errors='raise'`.
+    TypeError
+        If the input is not list-like, 1D, or scalar convertible to numeric,
+        such as nested lists or unsupported input types (e.g., dict).
+
     See Also
     --------
     DataFrame.astype : Cast argument to a specified dtype.


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index b501c2ea39..d2899e3838 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -30,7 +30,7 @@ jobs:
         env_file: [actions-310.yaml, actions-311.yaml, actions-312.yaml, actions-313.yaml]
         # Prevent the include jobs from overriding other jobs
         pattern: [""]
-        pandas_future_infer_string: ["0"]
+        pandas_future_infer_string: ["1"]
         include:
           - name: "Downstream Compat"
             env_file: actions-311-downstream_compat.yaml
@@ -45,6 +45,10 @@ jobs:
             env_file: actions-313-freethreading.yaml
             pattern: "not slow and not network and not single_cpu"
             platform: ubuntu-24.04
+          - name: "Without PyArrow"
+            env_file: actions-312.yaml
+            pattern: "not slow and not network and not single_cpu"
+            platform: ubuntu-24.04
           - name: "Locale: it_IT"
             env_file: actions-311.yaml
             pattern: "not slow and not network and not single_cpu"
@@ -67,13 +71,9 @@ jobs:
             # It will be temporarily activated during tests with locale.setlocale
             extra_loc: "zh_CN"
             platform: ubuntu-24.04
-          - name: "Future infer strings"
+          - name: "Past no infer strings"
             env_file: actions-312.yaml
-            pandas_future_infer_string: "1"
-            platform: ubuntu-24.04
-          - name: "Future infer strings (without pyarrow)"
-            env_file: actions-311.yaml
-            pandas_future_infer_string: "1"
+            pandas_future_infer_string: "0"
             platform: ubuntu-24.04
           - name: "Numpy Dev"
             env_file: actions-311-numpydev.yaml
@@ -83,7 +83,6 @@ jobs:
           - name: "Pyarrow Nightly"
             env_file: actions-311-pyarrownightly.yaml
             pattern: "not slow and not network and not single_cpu"
-            pandas_future_infer_string: "1"
             platform: ubuntu-24.04
       fail-fast: false
     name: ${{ matrix.name || format('{0} {1}', matrix.platform, matrix.env_file) }}
@@ -98,7 +97,7 @@ jobs:
       PYTEST_TARGET: ${{ matrix.pytest_target || 'pandas' }}
       # Clipboard tests
       QT_QPA_PLATFORM: offscreen
-      REMOVE_PYARROW: ${{ matrix.name == 'Future infer strings (without pyarrow)' && '1' || '0' }}
+      REMOVE_PYARROW: ${{ matrix.name == 'Without PyArrow' && '1' || '0' }}
     concurrency:
       # https://github.community/t/concurrecy-not-work-for-push/183068/7
       group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.pattern }}-${{ matrix.extra_apt || '' }}-${{ matrix.pandas_future_infer_string }}-${{ matrix.platform }}
diff --git a/pandas/tests/indexes/test_numpy_compat.py b/pandas/tests/indexes/test_numpy_compat.py
index ace78d7735..86d0ca1280 100644
--- a/pandas/tests/indexes/test_numpy_compat.py
+++ b/pandas/tests/indexes/test_numpy_compat.py
@@ -2,6 +2,7 @@ import numpy as np
 import pytest
 
 from pandas import (
+    BooleanDtype,
     CategoricalIndex,
     DatetimeIndex,
     Index,
@@ -14,7 +15,6 @@ from pandas.api.types import (
     is_complex_dtype,
     is_numeric_dtype,
 )
-from pandas.core.arrays import BooleanArray
 from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin
 
 
@@ -111,11 +111,10 @@ def test_numpy_ufuncs_other(index, func):
         if func in (np.isfinite, np.isinf, np.isnan):
             # numpy 1.18 changed isinf and isnan to not raise on dt64/td64
             result = func(index)
-            assert isinstance(result, np.ndarray)
 
             out = np.empty(index.shape, dtype=bool)
             func(index, out=out)
-            tm.assert_numpy_array_equal(out, result)
+            tm.assert_index_equal(Index(out), result)
         else:
             with tm.external_error_raised(TypeError):
                 func(index)
@@ -129,19 +128,20 @@ def test_numpy_ufuncs_other(index, func):
     ):
         # Results in bool array
         result = func(index)
+        assert isinstance(result, Index)
         if not isinstance(index.dtype, np.dtype):
             # e.g. Int64 we expect to get BooleanArray back
-            assert isinstance(result, BooleanArray)
+            assert isinstance(result.dtype, BooleanDtype)
         else:
-            assert isinstance(result, np.ndarray)
+            assert isinstance(result.dtype, np.dtype)
 
         out = np.empty(index.shape, dtype=bool)
         func(index, out=out)
 
         if not isinstance(index.dtype, np.dtype):
-            tm.assert_numpy_array_equal(out, result._data)
+            tm.assert_index_equal(result, Index(out, dtype="boolean"))
         else:
-            tm.assert_numpy_array_equal(out, result)
+            tm.assert_index_equal(result, Index(out))
 
     elif len(index) == 0:
         pass
diff --git a/pandas/tests/reshape/test_from_dummies.py b/pandas/tests/reshape/test_from_dummies.py
index c7b7992a78..dfb691c785 100644
--- a/pandas/tests/reshape/test_from_dummies.py
+++ b/pandas/tests/reshape/test_from_dummies.py
@@ -333,9 +333,7 @@ def test_no_prefix_string_cats_default_category(
 ):
     dummies = DataFrame({"a": [1, 0, 0], "b": [0, 1, 0]})
     result = from_dummies(dummies, default_category=default_category)
-    expected = DataFrame(expected)
-    if using_infer_string:
-        expected[""] = expected[""].astype("str")
+    expected = DataFrame(expected, dtype=dummies.columns.dtype)
     tm.assert_frame_equal(result, expected)
 
 
@@ -449,3 +447,31 @@ def test_maintain_original_index():
     result = from_dummies(df)
     expected = DataFrame({"": list("abca")}, index=list("abcd"))
     tm.assert_frame_equal(result, expected)
+
+
+def test_int_columns_with_float_default():
+    # https://github.com/pandas-dev/pandas/pull/60694
+    df = DataFrame(
+        {
+            3: [1, 0, 0],
+            4: [0, 1, 0],
+        },
+    )
+    with pytest.raises(ValueError, match="Trying to coerce float values to integers"):
+        from_dummies(df, default_category=0.5)
+
+
+def test_object_dtype_preserved():
+    # https://github.com/pandas-dev/pandas/pull/60694
+    # When the input has object dtype, the result should as
+    # well even when infer_string is True.
+    df = DataFrame(
+        {
+            "x": [1, 0, 0],
+            "y": [0, 1, 0],
+        },
+    )
+    df.columns = df.columns.astype("object")
+    result = from_dummies(df, default_category="z")
+    expected = DataFrame({"": ["x", "y", "z"]}, dtype="object")
+    tm.assert_frame_equal(result, expected)