pandas copy-on-write chained assignment behavior
Explains pandas copy-on-write behavior and chained assignment: when df[a][b] = x silently does nothing and how to write assignments that stick. Use when an assignment to a filtered dataframe does not persist, when you see SettingWithCopyWarning, or when migrating to pandas 3 copy-on-write semantics. Not for merge key mismatches, for read_csv parse errors, or for performance tuning of apply.
TL;DR
Never assign through chained indexing like df[mask]["col"] = x; always use .loc[mask, "col"] = x. With copy-on-write (default since pandas 3), chained assignment never writes through to the original frame, it fails silently instead of warning.
pandas copy-on-write chained assignment behaviorUse this when
- An assignment to a filtered or sliced dataframe does not stick
- You see SettingWithCopyWarning on pandas 2, or silence on pandas 3
- You are migrating code to copy-on-write semantics
Not for this skill when
- Merges fail on key mismatches
read_csvmisparses a columnapplyis too slow and needs vectorization
Steps
- Confirm the chained assignment is the problem. This pattern looks right but writes to a temporary:
df[df["status"] == "active"]["score"] = 0 # does nothing under CoW
print((df["status"] == "active").sum(), (df["score"] == 0).sum())Expected output: the filter matches rows but zero rows got the new value. The assignment went to a throwaway copy.
- Rewrite every chained assignment as a single
.loccall:
df.loc[df["status"] == "active", "score"] = 0
print((df.loc[df["status"] == "active", "score"] == 0).all())Expected output: True. One indexing operation, so pandas knows exactly which cells to update.
- For multi-step transforms, assign the intermediate explicitly instead of chaining:
filtered = df[df["status"] == "active"].copy()
filtered["score"] = filtered["score"] * 2Expected output: filtered is an independent frame you own, and assignments to it always work. Under copy-on-write the .copy() is cheap until you actually modify.
- If you intentionally want a view-like workflow across function calls, return the new frame instead of mutating:
def boost_scores(frame):
out = frame.copy()
out.loc[out["status"] == "active", "score"] *= 2
return outExpected output: the caller's frame is untouched and the returned frame has the change. Copy-on-write makes this pattern memory-cheap.
- Check whether copy-on-write is active in your environment:
print(pd.options.mode.copy_on_write)Expected output: True on pandas 3 and later (it is the only mode), configurable on pandas 2.x. Code that relies on old view-mutation behavior breaks here.
Variant phrasings
SettingWithCopyWarning pandas 2
The warning was the old signal for this exact problem. Fix it the same way: .loc for in-place writes, .copy() for owned intermediates. Under pandas 3 the warning is gone because the write simply never happens.
pandas assignment not working on filtered dataframe
Same root cause. Any df[boolean_mask]["col"] = value is two indexing operations and the second one writes to a temporary.
chained assignment with iloc
df.iloc[0:5]["col"] = x has the same problem. Use df.iloc[0:5, df.columns.get_loc("col")] = x for a single operation.
Why it happens
df[mask] returns a new object, and ["col"] = x on it assigns into that temporary, which is immediately discarded. Before copy-on-write, pandas sometimes returned a view, so the write occasionally leaked through to the original, which is why the old behavior was unpredictable. Copy-on-write removed the view case entirely: chained assignment now reliably does nothing.
Edge cases
- Method chains ending in assignment (
df.query(...)["col"] = x) are still chained assignment, rewrite with.loc. df["new_col"] = ...on the full frame (single indexing) is always fine and is not chaining.- In-place methods like
fillna(inplace=True)on a slice hit the same trap, assign the result instead. - Libraries that mutate frames you pass in may silently stop working under CoW, check their changelogs.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstKzPAIBKv1wgG_He5-AYww