diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index f636ee1806c9b..e80aa0ace6bc2 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -41,6 +41,7 @@ Exceptions and warnings errors.ParserError errors.ParserWarning errors.PerformanceWarning + errors.SettingWithCopyError errors.SpecificationError errors.UnsortedIndexError errors.UnsupportedFunctionCall diff --git a/doc/source/user_guide/indexing.rst b/doc/source/user_guide/indexing.rst index 3c08b5a498eea..f939945fc6cda 100644 --- a/doc/source/user_guide/indexing.rst +++ b/doc/source/user_guide/indexing.rst @@ -1885,7 +1885,7 @@ chained indexing expression, you can set the :ref:`option ` ``mode.chained_assignment`` to one of these values: * ``'warn'``, the default, means a ``SettingWithCopyWarning`` is printed. -* ``'raise'`` means pandas will raise a ``SettingWithCopyException`` +* ``'raise'`` means pandas will raise a ``SettingWithCopyError`` you have to deal with. * ``None`` will suppress the warnings entirely. @@ -1953,7 +1953,7 @@ Last, the subsequent example will **not** work at all, and so should be avoided: >>> dfd.loc[0]['a'] = 1111 Traceback (most recent call last) ... - SettingWithCopyException: + SettingWithCopyError: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_index,col_indexer] = value instead diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 4a0b9a97a9d11..4ceb833214a79 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -151,7 +151,7 @@ Other enhancements - A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`) - Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`) - ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`) -- :class:`DataError` and :class:`SpecificationError` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`DataError`, :class:`SpecificationError`, and :class:`SettingWithCopyError` are now exposed in ``pandas.errors`` (:issue:`27656`) .. --------------------------------------------------------------------------- .. _whatsnew_150.notable_bug_fixes: diff --git a/pandas/core/common.py b/pandas/core/common.py index eeb18759fc72c..05eb101dabb98 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -58,10 +58,6 @@ from pandas import Index -class SettingWithCopyError(ValueError): - pass - - class SettingWithCopyWarning(Warning): pass diff --git a/pandas/core/generic.py b/pandas/core/generic.py index f98fbe79861aa..d5b55a89f28df 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -69,6 +69,7 @@ from pandas.errors import ( AbstractMethodError, InvalidIndexError, + SettingWithCopyError, ) from pandas.util._decorators import ( deprecate_kwarg, @@ -3949,7 +3950,7 @@ def _check_setitem_copy(self, t="setting", force=False): ) if value == "raise": - raise com.SettingWithCopyError(t) + raise SettingWithCopyError(t) elif value == "warn": warnings.warn(t, com.SettingWithCopyWarning, stacklevel=find_stack_level()) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index a948f0f46e21a..b2a1309d49be3 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -267,3 +267,24 @@ class SpecificationError(Exception): >>> df.groupby('A').agg(['min', 'min']) # doctest: +SKIP ... # SpecificationError: nested renamer is not supported """ + + +class SettingWithCopyError(ValueError): + """ + Exception is raised when trying to set on a copied slice from a dataframe and + the mode.chained_assignment is set to 'raise.' This can happen unintentionally + when chained indexing. + + For more information, see 'Evaluation order matters' on + https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html + + For more information, see 'Indexing view versus copy' on + https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html + + Examples + -------- + >>> pd.options.mode.chained_assignment = 'raise' + >>> df = pd.DataFrame({'A': [1, 1, 1, 2, 2]}, columns=['A']) + >>> df.loc[0:3]['A'] = 'a' # doctest: +SKIP + ... # SettingWithCopyError: A value is trying to be set on a copy of a... + """ diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index 5cac63e41da67..0fbf375e441ac 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -8,7 +8,10 @@ import pytest from pandas._libs import iNaT -from pandas.errors import InvalidIndexError +from pandas.errors import ( + InvalidIndexError, + SettingWithCopyError, +) import pandas.util._test_decorators as td from pandas.core.dtypes.common import is_integer @@ -27,7 +30,6 @@ notna, ) import pandas._testing as tm -import pandas.core.common as com # We pass through a TypeError raised by numpy _slice_msg = "slice indices must be integers or None or have an __index__ method" @@ -303,7 +305,7 @@ def test_setitem(self, float_frame): smaller = float_frame[:2] msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): smaller["col10"] = ["1", "2"] assert smaller["col10"].dtype == np.object_ @@ -546,7 +548,7 @@ def test_fancy_getitem_slice_mixed(self, float_frame, float_string_frame): assert np.shares_memory(sliced["C"]._values, float_frame["C"]._values) msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): sliced.loc[:, "C"] = 4.0 assert (float_frame["C"] == 4).all() @@ -1003,7 +1005,7 @@ def test_iloc_row_slice_view(self, using_array_manager): assert np.shares_memory(df[2], subset[2]) msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): subset.loc[:, 2] = 0.0 exp_col = original[2].copy() @@ -1046,7 +1048,7 @@ def test_iloc_col_slice_view(self, using_array_manager): # and that we are setting a copy msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): subset.loc[:, 8] = 0.0 assert (df[8] == 0).all() diff --git a/pandas/tests/frame/indexing/test_xs.py b/pandas/tests/frame/indexing/test_xs.py index c6938abb57d64..898722d6d77ae 100644 --- a/pandas/tests/frame/indexing/test_xs.py +++ b/pandas/tests/frame/indexing/test_xs.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from pandas.errors import SettingWithCopyError + from pandas import ( DataFrame, Index, @@ -12,7 +14,6 @@ concat, ) import pandas._testing as tm -import pandas.core.common as com from pandas.tseries.offsets import BDay @@ -120,7 +121,7 @@ def test_xs_view(self, using_array_manager): # INFO(ArrayManager) with ArrayManager getting a row as a view is # not possible msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): dm.xs(2)[:] = 20 assert not (dm.xs(2) == 20).any() else: @@ -183,7 +184,7 @@ def test_xs_setting_with_copy_error(self, multiindex_dataframe_random_data): # setting this will give a SettingWithCopyError # as we are trying to write a view msg = "A value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): result[:] = 10 def test_xs_setting_with_copy_error_multiple(self, four_level_index_dataframe): @@ -194,7 +195,7 @@ def test_xs_setting_with_copy_error_multiple(self, four_level_index_dataframe): # setting this will give a SettingWithCopyError # as we are trying to write a view msg = "A value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): result[:] = 10 @pytest.mark.parametrize("key, level", [("one", "second"), (["one"], ["second"])]) diff --git a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py index 6ccd44e698a8a..479cd9952f75b 100644 --- a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py +++ b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py @@ -1,6 +1,7 @@ import numpy as np import pytest +from pandas.errors import SettingWithCopyError import pandas.util._test_decorators as td from pandas import ( @@ -9,7 +10,6 @@ Series, ) import pandas._testing as tm -import pandas.core.common as com def test_detect_chained_assignment(): @@ -30,7 +30,7 @@ def test_detect_chained_assignment(): zed = DataFrame(events, index=["a", "b"], columns=multiind) msg = "A value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): zed["eyes"]["right"].fillna(value=555, inplace=True) diff --git a/pandas/tests/indexing/multiindex/test_setitem.py b/pandas/tests/indexing/multiindex/test_setitem.py index a9af83fa632f2..20569061cfa4c 100644 --- a/pandas/tests/indexing/multiindex/test_setitem.py +++ b/pandas/tests/indexing/multiindex/test_setitem.py @@ -1,6 +1,7 @@ import numpy as np import pytest +from pandas.errors import SettingWithCopyError import pandas.util._test_decorators as td import pandas as pd @@ -14,7 +15,6 @@ notna, ) import pandas._testing as tm -import pandas.core.common as com def assert_equal(a, b): @@ -491,7 +491,7 @@ def test_frame_setitem_copy_raises(multiindex_dataframe_random_data): # will raise/warn as its chained assignment df = multiindex_dataframe_random_data.T msg = "A value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["foo"]["one"] = 2 @@ -500,7 +500,7 @@ def test_frame_setitem_copy_no_write(multiindex_dataframe_random_data): expected = frame df = frame.copy() msg = "A value is trying to be set on a copy of a slice from a DataFrame" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["foo"]["one"] = 2 result = df diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py index c8837a617bd9a..c6b36b851a838 100644 --- a/pandas/tests/indexing/test_chaining_and_caching.py +++ b/pandas/tests/indexing/test_chaining_and_caching.py @@ -3,6 +3,7 @@ import numpy as np import pytest +from pandas.errors import SettingWithCopyError import pandas.util._test_decorators as td import pandas as pd @@ -182,10 +183,10 @@ def test_detect_chained_assignment_raises(self, using_array_manager): assert df._is_copy is None if not using_array_manager: - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["A"][0] = -5 - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["A"][1] = np.nan assert df["A"]._is_copy is None @@ -210,7 +211,7 @@ def test_detect_chained_assignment_fails(self): } ) - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.loc[0]["A"] = -5 @pytest.mark.arm_slow @@ -225,7 +226,7 @@ def test_detect_chained_assignment_doc_example(self): ) assert df._is_copy is None - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): indexer = df.a.str.startswith("o") df[indexer]["c"] = 42 @@ -235,11 +236,11 @@ def test_detect_chained_assignment_object_dtype(self, using_array_manager): expected = DataFrame({"A": [111, "bbb", "ccc"], "B": [1, 2, 3]}) df = DataFrame({"A": ["aaa", "bbb", "ccc"], "B": [1, 2, 3]}) - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.loc[0]["A"] = 111 if not using_array_manager: - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["A"][0] = 111 df.loc[0, "A"] = 111 @@ -360,7 +361,7 @@ def test_detect_chained_assignment_undefined_column(self): df = DataFrame(np.arange(0, 9), columns=["count"]) df["group"] = "b" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.iloc[0:5]["group"] = "a" @pytest.mark.arm_slow @@ -376,14 +377,14 @@ def test_detect_chained_assignment_changing_dtype(self, using_array_manager): } ) - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.loc[2]["D"] = "foo" - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.loc[2]["C"] = "foo" if not using_array_manager: - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df["C"][2] = "foo" else: # INFO(ArrayManager) for ArrayManager it doesn't matter if it's @@ -399,7 +400,7 @@ def test_setting_with_copy_bug(self): ) mask = pd.isna(df.c) - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df[["c"]][mask] = df[["b"]][mask] def test_setting_with_copy_bug_no_warning(self): @@ -418,7 +419,7 @@ def test_detect_chained_assignment_warnings_errors(self): df.loc[0]["A"] = 111 with option_context("chained_assignment", "raise"): - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): df.loc[0]["A"] = 111 def test_detect_chained_assignment_warnings_filter_and_dupe_cols(self): diff --git a/pandas/tests/series/accessors/test_dt_accessor.py b/pandas/tests/series/accessors/test_dt_accessor.py index 41b5e55e75213..afb6d0f19daca 100644 --- a/pandas/tests/series/accessors/test_dt_accessor.py +++ b/pandas/tests/series/accessors/test_dt_accessor.py @@ -12,6 +12,7 @@ import pytz from pandas._libs.tslibs.timezones import maybe_get_tz +from pandas.errors import SettingWithCopyError from pandas.core.dtypes.common import ( is_integer_dtype, @@ -37,7 +38,6 @@ PeriodArray, TimedeltaArray, ) -import pandas.core.common as com ok_for_period = PeriodArray._datetimelike_ops ok_for_period_methods = ["strftime", "to_timestamp", "asfreq"] @@ -288,7 +288,7 @@ def test_dt_accessor_not_writeable(self): # trying to set a copy msg = "modifications to a property of a datetimelike.+not supported" with pd.option_context("chained_assignment", "raise"): - with pytest.raises(com.SettingWithCopyError, match=msg): + with pytest.raises(SettingWithCopyError, match=msg): ser.dt.hour[0] = 5 @pytest.mark.parametrize( diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index 140297297fdbc..cc6421c42c996 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -21,6 +21,7 @@ "NumbaUtilError", "DataError", "SpecificationError", + "SettingWithCopyError", ], ) def test_exception_importable(exc):