Skip to content

ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment

Have you ever seen this, shrugged because your code kept running, and then discovered that your data never changed?

df = pd.DataFrame({'city': ['Oslo', 'Lima', 'Pune'],
                   'temp': [5.0, 18.0, 31.0]})

df[df['temp'] > 30]['temp'] = 30
df
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).

   city  temp
0  Oslo   5.0
1  Lima  18.0
2  Pune  31.0

Pune is still at 31.0. The assignment didn't happen.

The short answer is to do the selection and the assignment in a single step, with loc:

df.loc[df['temp'] > 30, 'temp'] = 30
df
   city  temp
0  Oslo   5.0
1  Lima  18.0
2  Pune  30.0

Despite the name, it is not an error

This is the single most important thing to know about ChainedAssignmentError: it's a warning. The class inherits from Python's Warning, so nothing is raised, nothing stops, and the next line of your program runs as though all were well. The name says "error" because the outcome is always wrong, but Pandas delivers it as a warning.

In a notebook, that means a pink box you can scroll past. In a script, it means a line on stderr that nobody reads, and a result that's quietly incorrect. If you'd rather Pandas stop you, you can promote it to a real exception:

import warnings
warnings.simplefilter('error', pd.errors.ChainedAssignmentError)

I'd recommend doing that in any code base you're migrating to Pandas 3.

What "chained" means

Chained assignment is any assignment that goes through two indexing operations, one after the other. df[df['temp'] > 30] is the first. It produces a new data frame containing the hot rows. ['temp'] = 30 is the second, and it modifies that new data frame, which you never stored in a variable and which disappears immediately afterward.

Every one of these forms has the same problem:

df[df['temp'] > 30]['temp'] = 30      # filter, then column
df.loc[df['temp'] > 30]['temp'] = 30  # loc, then column
df['temp'][0] = -1                    # column, then row
df.iloc[0]['temp'] = -1               # row, then column

And each has a one-step equivalent. Use loc with both a row selector and a column name, or at for a single cell:

df.at[0, 'temp'] = -1

Why this used to work (sometimes)

In Pandas 2, df['temp'][0] = -1 did update df, because df['temp'] usually handed back a view onto the same memory. Filtering with a boolean mask, by contrast, usually produced a copy, so that form usually didn't work. Which one you got depended on internal details you couldn't see, which is why Pandas 2 printed the famous SettingWithCopyWarning for both: it couldn't promise either way. I walk through that warning, and how to avoid it, in Understanding (and avoiding) the SettingWithCopyWarning in Pandas.

Pandas 3 turns on copy-on-write for everything. Every indexing operation now behaves as a copy, and memory is shared behind the scenes only until someone writes to it. You can see the difference directly:

t = df['temp']
t.iloc[1] = 0.0
df['temp'].tolist()
[-1.0, 18.0, 30.0]

Changing t no longer changes df. That rule is simpler and predictable, and it means chained assignment now fails every time, rather than some of the time. The dangerous case is code that used to work in Pandas 2 and now silently doesn't.

SettingWithCopyWarning is gone

If you've spent years learning to fear SettingWithCopyWarning, you can let it go. It doesn't exist in Pandas 3, and there is no longer any such attribute in pandas.errors. In particular, this pattern, which used to warn, is now completely fine:

hot = df[df['temp'] > 10]
hot['temp_f'] = hot['temp'] * 9 / 5 + 32

hot is its own data frame, you stored it in a variable, and you're modifying it on purpose. No warning, and df is untouched. ChainedAssignmentError is specifically about modifying something you never kept.

I explain what changed, and why copy-on-write makes the old warning unnecessary, in SettingWithCopyWarning? Not in Pandas 3, thanks to "copy on write".

The inplace variant

There's a second version of the message, which adds "using an inplace method":

df['temp'].fillna(0, inplace=True)
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.

It's the same chain. df['temp'] produces a copy, and fillna modifies that copy in place. The fix is to drop inplace and assign the result back, which is clearer anyway:

df['temp'] = df['temp'].fillna(0)

The same applies to replace, clip, interpolate, and every other method with an inplace parameter. For more on where inplace stands now, see inplace=True in Pandas 3: Better, worse, and still confusing.

Practice

See loc in Pandas for selecting and assigning in one step on real data, and fillna in Pandas for the inplace version of this trap. Then try the loc exercise.