diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index bac567b537edc..475d48be01a9e 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -115,6 +115,7 @@ Deprecations - Deprecated 'method', 'limit', and 'fill_axis' keywords in :meth:`DataFrame.align` and :meth:`Series.align`, explicitly call ``fillna`` on the alignment results instead (:issue:`51856`) - Deprecated 'broadcast_axis' keyword in :meth:`Series.align` and :meth:`DataFrame.align`, upcast before calling ``align`` with ``left = DataFrame({col: left for col in right.columns}, index=right.index)`` (:issue:`51856`) - Deprecated the 'axis' keyword in :meth:`.GroupBy.idxmax`, :meth:`.GroupBy.idxmin`, :meth:`.GroupBy.fillna`, :meth:`.GroupBy.take`, :meth:`.GroupBy.skew`, :meth:`.GroupBy.rank`, :meth:`.GroupBy.cumprod`, :meth:`.GroupBy.cumsum`, :meth:`.GroupBy.cummax`, :meth:`.GroupBy.cummin`, :meth:`.GroupBy.pct_change`, :meth:`GroupBy.diff`, :meth:`.GroupBy.shift`, and :meth:`DataFrameGroupBy.corrwith`; for ``axis=1`` operate on the underlying :class:`DataFrame` instead (:issue:`50405`, :issue:`51046`) +- Deprecated logical operations (``|``, ``&``, ``^``) between pandas objects and dtype-less sequences (e.g. ``list``, ``tuple``), wrap a sequence in a :class:`Series` or numpy array before operating instead (:issue:`51521`) - Deprecated :meth:`DataFrame.swapaxes` and :meth:`Series.swapaxes`, use :meth:`DataFrame.transpose` or :meth:`Series.transpose` instead (:issue:`51946`) - diff --git a/pandas/core/ops/array_ops.py b/pandas/core/ops/array_ops.py index bae2ab15f3696..13eb526cff209 100644 --- a/pandas/core/ops/array_ops.py +++ b/pandas/core/ops/array_ops.py @@ -11,6 +11,7 @@ TYPE_CHECKING, Any, ) +import warnings import numpy as np @@ -22,6 +23,7 @@ ops as libops, ) from pandas._libs.tslibs import BaseOffset +from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.cast import ( construct_1d_object_array_from_listlike, @@ -416,6 +418,14 @@ def fill_bool(x, left=None): right = lib.item_from_zerodim(right) if is_list_like(right) and not hasattr(right, "dtype"): # e.g. list, tuple + warnings.warn( + "Logical ops (and, or, xor) between Pandas objects and dtype-less " + "sequences (e.g. list, tuple) are deprecated and will raise in a " + "future version. Wrap the object in a Series, Index, or np.array " + "before operating instead.", + FutureWarning, + stacklevel=find_stack_level(), + ) right = construct_1d_object_array_from_listlike(right) # NB: We assume extract_array has already been called on left and right diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index fa72bf4368b69..98fecca0c43ca 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -801,6 +801,14 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators) name = op.__name__.strip("_") is_logical = name in ["and", "rand", "xor", "rxor", "or", "ror"] + msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + warn = None + if box in [list, tuple] and is_logical: + warn = FutureWarning + right = box(right) if flex: if is_logical: @@ -809,7 +817,8 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators) result = getattr(left, name)(right) else: # GH#37374 logical ops behaving as set ops deprecated - result = op(left, right) + with tm.assert_produces_warning(warn, match=msg): + result = op(left, right) assert isinstance(result, Series) if box in [Index, Series]: diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py index 0d661f19087e6..ccd934c2f17bb 100644 --- a/pandas/tests/series/test_logical_ops.py +++ b/pandas/tests/series/test_logical_ops.py @@ -86,6 +86,11 @@ def test_logical_operators_int_dtype_with_float(self): # GH#9016: support bitwise op for integer types s_0123 = Series(range(4), dtype="int64") + warn_msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + msg = "Cannot perform.+with a dtyped.+array and scalar of type" with pytest.raises(TypeError, match=msg): s_0123 & np.NaN @@ -93,7 +98,8 @@ def test_logical_operators_int_dtype_with_float(self): s_0123 & 3.14 msg = "unsupported operand type.+for &:" with pytest.raises(TypeError, match=msg): - s_0123 & [0.1, 4, 3.14, 2] + with tm.assert_produces_warning(FutureWarning, match=warn_msg): + s_0123 & [0.1, 4, 3.14, 2] with pytest.raises(TypeError, match=msg): s_0123 & np.array([0.1, 4, 3.14, 2]) with pytest.raises(TypeError, match=msg): @@ -101,11 +107,18 @@ def test_logical_operators_int_dtype_with_float(self): def test_logical_operators_int_dtype_with_str(self): s_1111 = Series([1] * 4, dtype="int8") + + warn_msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + msg = "Cannot perform 'and_' with a dtyped.+array and scalar of type" with pytest.raises(TypeError, match=msg): s_1111 & "a" with pytest.raises(TypeError, match="unsupported operand.+for &"): - s_1111 & ["a", "b", "c", "d"] + with tm.assert_produces_warning(FutureWarning, match=warn_msg): + s_1111 & ["a", "b", "c", "d"] def test_logical_operators_int_dtype_with_bool(self): # GH#9016: support bitwise op for integer types @@ -116,10 +129,16 @@ def test_logical_operators_int_dtype_with_bool(self): result = s_0123 & False tm.assert_series_equal(result, expected) - result = s_0123 & [False] + warn_msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + with tm.assert_produces_warning(FutureWarning, match=warn_msg): + result = s_0123 & [False] tm.assert_series_equal(result, expected) - result = s_0123 & (False,) + with tm.assert_produces_warning(FutureWarning, match=warn_msg): + result = s_0123 & (False,) tm.assert_series_equal(result, expected) result = s_0123 ^ False @@ -157,8 +176,14 @@ def test_logical_ops_bool_dtype_with_ndarray(self): left = Series([True, True, True, False, True]) right = [True, False, None, True, np.nan] + msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + expected = Series([True, False, False, False, False]) - result = left & right + with tm.assert_produces_warning(FutureWarning, match=msg): + result = left & right tm.assert_series_equal(result, expected) result = left & np.array(right) tm.assert_series_equal(result, expected) @@ -168,7 +193,8 @@ def test_logical_ops_bool_dtype_with_ndarray(self): tm.assert_series_equal(result, expected) expected = Series([True, True, True, True, True]) - result = left | right + with tm.assert_produces_warning(FutureWarning, match=msg): + result = left | right tm.assert_series_equal(result, expected) result = left | np.array(right) tm.assert_series_equal(result, expected) @@ -178,7 +204,8 @@ def test_logical_ops_bool_dtype_with_ndarray(self): tm.assert_series_equal(result, expected) expected = Series([False, True, True, True, True]) - result = left ^ right + with tm.assert_produces_warning(FutureWarning, match=msg): + result = left ^ right tm.assert_series_equal(result, expected) result = left ^ np.array(right) tm.assert_series_equal(result, expected) @@ -231,7 +258,13 @@ def test_scalar_na_logical_ops_corners(self): expected = Series(True, index=s.index) expected[::2] = False - result = s & list(s) + + msg = ( + r"Logical ops \(and, or, xor\) between Pandas objects and " + "dtype-less sequences" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): + result = s & list(s) tm.assert_series_equal(result, expected) def test_scalar_na_logical_ops_corners_aligns(self):