Repository: pandas-dev/pandas
Pull request #58241 (2024-04-12 21:06:29)

DEPR: object casting in Series logical ops in non-bool cases
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] 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 e05cc87d1a..50643454bb 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -208,6 +208,7 @@ Removal of prior version deprecations/changes
 - :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
 - All arguments except ``name`` in :meth:`Index.rename` are now keyword only (:issue:`56493`)
 - All arguments except the first ``path``-like argument in IO writers are now keyword only (:issue:`54229`)
+- Disallow automatic casting to object in :class:`Series` logical operations (``&``, ``^``, ``||``) between series with mismatched indexes and dtypes other than ``object`` or ``bool`` (:issue:`52538`)
 - Disallow calling :meth:`Series.replace` or :meth:`DataFrame.replace` without a ``value`` and with non-dict-like ``to_replace`` (:issue:`33302`)
 - Disallow constructing a :class:`arrays.SparseArray` with scalar data (:issue:`53039`)
 - Disallow non-standard (``np.ndarray``, :class:`Index`, :class:`ExtensionArray`, or :class:`Series`) to :func:`isin`, :func:`unique`, :func:`factorize` (:issue:`52986`)
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 0f796964eb..ab24b224b0 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -5859,17 +5859,12 @@ class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
                         object,
                         np.bool_,
                     ):
-                        warnings.warn(
-                            "Operation between non boolean Series with different "
-                            "indexes will no longer return a boolean result in "
-                            "a future version. Cast both Series to object type "
-                            "to maintain the prior behavior.",
-                            FutureWarning,
-                            stacklevel=find_stack_level(),
-                        )
-                    # to keep original value's dtype for bool ops
-                    left = left.astype(object)
-                    right = right.astype(object)
+                        pass
+                        # GH#52538 no longer cast in these cases
+                    else:
+                        # to keep original value's dtype for bool ops
+                        left = left.astype(object)
+                        right = right.astype(object)
 
                 left, right = left.align(right)
 


Test patch:
diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py
index b76b69289b..dfc3309a8e 100644
--- a/pandas/tests/series/test_logical_ops.py
+++ b/pandas/tests/series/test_logical_ops.py
@@ -233,26 +233,22 @@ class TestSeriesLogicalOps:
 
         # s_0123 will be all false now because of reindexing like s_tft
         expected = Series([False] * 7, index=[0, 1, 2, 3, "a", "b", "c"])
-        with tm.assert_produces_warning(FutureWarning):
-            result = s_tft & s_0123
+        result = s_tft & s_0123
         tm.assert_series_equal(result, expected)
 
-        # GH 52538: Deprecate casting to object type when reindex is needed;
+        # GH#52538: no longer to object type when reindex is needed;
         # matches DataFrame behavior
-        expected = Series([False] * 7, index=[0, 1, 2, 3, "a", "b", "c"])
-        with tm.assert_produces_warning(FutureWarning):
-            result = s_0123 & s_tft
-        tm.assert_series_equal(result, expected)
+        msg = r"unsupported operand type\(s\) for &: 'float' and 'bool'"
+        with pytest.raises(TypeError, match=msg):
+            s_0123 & s_tft
 
         s_a0b1c0 = Series([1], list("b"))
 
-        with tm.assert_produces_warning(FutureWarning):
-            res = s_tft & s_a0b1c0
+        res = s_tft & s_a0b1c0
         expected = s_tff.reindex(list("abc"))
         tm.assert_series_equal(res, expected)
 
-        with tm.assert_produces_warning(FutureWarning):
-            res = s_tft | s_a0b1c0
+        res = s_tft | s_a0b1c0
         expected = s_tft.reindex(list("abc"))
         tm.assert_series_equal(res, expected)
 
@@ -405,27 +401,24 @@ class TestSeriesLogicalOps:
         tm.assert_series_equal(result, expected)
 
         # vs non-matching
-        with tm.assert_produces_warning(FutureWarning):
-            result = a & Series([1], ["z"])
+        result = a & Series([1], ["z"])
         expected = Series([False, False, False, False], list("abcz"))
         tm.assert_series_equal(result, expected)
 
-        with tm.assert_produces_warning(FutureWarning):
-            result = a | Series([1], ["z"])
+        result = a | Series([1], ["z"])
         expected = Series([True, True, False, False], list("abcz"))
         tm.assert_series_equal(result, expected)
 
         # identity
         # we would like s[s|e] == s to hold for any e, whether empty or not
-        with tm.assert_produces_warning(FutureWarning):
-            for e in [
-                empty.copy(),
-                Series([1], ["z"]),
-                Series(np.nan, b.index),
-                Series(np.nan, a.index),
-            ]:
-                result = a[a | e]
-                tm.assert_series_equal(result, a[a])
+        for e in [
+            empty.copy(),
+            Series([1], ["z"]),
+            Series(np.nan, b.index),
+            Series(np.nan, a.index),
+        ]:
+            result = a[a | e]
+            tm.assert_series_equal(result, a[a])
 
         for e in [Series(["z"])]:
             warn = FutureWarning if using_infer_string else None
@@ -519,7 +512,6 @@ class TestSeriesLogicalOps:
         tm.assert_frame_equal(s3.to_frame() | s4.to_frame(), exp_or1.to_frame())
         tm.assert_frame_equal(s4.to_frame() | s3.to_frame(), exp_or.to_frame())
 
-    @pytest.mark.xfail(reason="Will pass once #52839 deprecation is enforced")
     def test_int_dtype_different_index_not_bool(self):
         # GH 52500
         ser1 = Series([1, 2, 3], index=[10, 11, 23], name="a")