Skip to content

Backport PR #51926 on branch 2.0.x (CoW: change ChainedAssignmentError exception to a warning) #51986

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions doc/source/user_guide/copy_on_write.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ two subsequent indexing operations, e.g.
The column ``foo`` is updated where the column ``bar`` is greater than 5.
This violates the CoW principles though, because it would have to modify the
view ``df["foo"]`` and ``df`` in one step. Hence, chained assignment will
consistently never work and raise a ``ChainedAssignmentError`` with CoW enabled:
consistently never work and raise a ``ChainedAssignmentError`` warning
with CoW enabled:

.. ipython:: python
:okexcept:
:okwarning:

df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"][df["bar"] > 5] = 100
Expand Down
4 changes: 2 additions & 2 deletions doc/source/whatsnew/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ Copy-on-Write improvements
of those Series objects for the columns of the DataFrame (:issue:`50777`)

- Trying to set values using chained assignment (for example, ``df["a"][1:3] = 0``)
will now always raise an exception when Copy-on-Write is enabled. In this mode,
will now always raise an warning when Copy-on-Write is enabled. In this mode,
chained assignment can never work because we are always setting into a temporary
object that is the result of an indexing operation (getitem), which under
Copy-on-Write always behaves as a copy. Thus, assigning through a chain
can never update the original Series or DataFrame. Therefore, an informative
error is raised to the user instead of silently doing nothing (:issue:`49467`)
warning is raised to the user to avoid silently doing nothing (:issue:`49467`)

- :meth:`DataFrame.replace` will now respect the Copy-on-Write mechanism
when ``inplace=True``.
Expand Down
4 changes: 2 additions & 2 deletions pandas/_testing/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ def raises_chained_assignment_error():

return nullcontext()
else:
import pytest
from pandas._testing import assert_produces_warning

return pytest.raises(
return assert_produces_warning(
ChainedAssignmentError,
match=(
"A value is trying to be set on a copy of a DataFrame or Series "
Expand Down
4 changes: 3 additions & 1 deletion pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3904,7 +3904,9 @@ def isetitem(self, loc, value) -> None:
def __setitem__(self, key, value):
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= 3:
raise ChainedAssignmentError(_chained_assignment_msg)
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
)

key = com.apply_if_callable(key, self)

Expand Down
5 changes: 4 additions & 1 deletion pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
cast,
final,
)
import warnings

import numpy as np

Expand Down Expand Up @@ -831,7 +832,9 @@ def _ensure_listlike_indexer(self, key, axis=None, value=None) -> None:
def __setitem__(self, key, value) -> None:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self.obj) <= 2:
raise ChainedAssignmentError(_chained_assignment_msg)
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
)

check_dict_or_set_indexers(key)
if isinstance(key, tuple):
Expand Down
4 changes: 3 additions & 1 deletion pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,9 @@ def _get_value(self, label, takeable: bool = False):
def __setitem__(self, key, value) -> None:
if not PYPY and using_copy_on_write():
if sys.getrefcount(self) <= 3:
raise ChainedAssignmentError(_chained_assignment_msg)
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
)

check_dict_or_set_indexers(key)
key = com.apply_if_callable(key, self)
Expand Down
4 changes: 2 additions & 2 deletions pandas/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ class SettingWithCopyWarning(Warning):
"""


class ChainedAssignmentError(ValueError):
class ChainedAssignmentError(Warning):
"""
Exception raised when trying to set using chained assignment.
Warning raised when trying to set using chained assignment.

When the ``mode.copy_on_write`` option is enabled, chained assignment can
never work. In such a situation, we are always setting into a temporary
Expand Down
12 changes: 6 additions & 6 deletions pandas/tests/io/test_spss.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import numpy as np
import pytest

import pandas.util._test_decorators as td

import pandas as pd
import pandas._testing as tm

pyreadstat = pytest.importorskip("pyreadstat")


@td.skip_copy_on_write_not_yet_implemented
# TODO(CoW) - detection of chained assignment in cython
# https://github.com/pandas-dev/pandas/issues/51315
@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
@pytest.mark.parametrize("path_klass", [lambda p: p, Path])
def test_spss_labelled_num(path_klass, datapath):
# test file from the Haven project (https://haven.tidyverse.org/)
Expand All @@ -27,7 +27,7 @@ def test_spss_labelled_num(path_klass, datapath):
tm.assert_frame_equal(df, expected)


@td.skip_copy_on_write_not_yet_implemented
@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
def test_spss_labelled_num_na(datapath):
# test file from the Haven project (https://haven.tidyverse.org/)
fname = datapath("io", "data", "spss", "labelled-num-na.sav")
Expand All @@ -42,7 +42,7 @@ def test_spss_labelled_num_na(datapath):
tm.assert_frame_equal(df, expected)


@td.skip_copy_on_write_not_yet_implemented
@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
def test_spss_labelled_str(datapath):
# test file from the Haven project (https://haven.tidyverse.org/)
fname = datapath("io", "data", "spss", "labelled-str.sav")
Expand All @@ -57,7 +57,7 @@ def test_spss_labelled_str(datapath):
tm.assert_frame_equal(df, expected)


@td.skip_copy_on_write_not_yet_implemented
@pytest.mark.filterwarnings("ignore::pandas.errors.ChainedAssignmentError")
def test_spss_umlauts(datapath):
# test file from the Haven project (https://haven.tidyverse.org/)
fname = datapath("io", "data", "spss", "umlauts.sav")
Expand Down