CoW removes SettingWithCopyWarning ambiguity and silent sharing bugs, but changes behaviour older code relied on.
Historically, whether a pandas operation returned a view (sharing data with the original) or a copy depended on internals. Modifying a result might or might not change the parent, and SettingWithCopyWarning tried, imperfectly, to warn you.
Copy-on-write (CoW) replaces that with one simple rule: every DataFrame or Series derived from another behaves as a copy. Modifying it never changes the parent, and modifying the parent never changes it. CoW is the only mode in pandas 3.0; in pandas 2.2 enable it with pd.options.mode.copy_on_write = True, or set it to "warn" to find code whose behaviour will change.
It's also faster. Internally, derived objects still share memory as lazy views. A real copy happens only when one of them is written to, and only for the data being modified. So operations like reset_index, rename, set_index or selecting columns no longer copy eagerly, and defensive .copy() calls become unnecessary.
Consequences for your code:
- Chained assignment never works:
df["a"][mask] = 0ordf[df.x > 0]["y"] = 1modify a temporary object and leavedfunchanged; pandas emits aChainedAssignmentErrorwarning. Writedf.loc[mask, "a"] = 0. - Updating a column extracted earlier (
s = df["a"]; s.iloc[0] = 99) no longer changesdf. inplace=Trueon a column selection, likedf["a"].fillna(0, inplace=True), doesn't touchdf. Usedf["a"] = df["a"].fillna(0).df.to_numpy()ands.valuesmay return read-only arrays when they share memory with the frame; call.copy()on the array to write to it.
The mental model becomes simple: to change a DataFrame, assign to it directly with df[...] = or df.loc[...] =. Everything else produces a new object.
Example
import pandas as pd
if pd.__version__.startswith("2."):
pd.options.mode.copy_on_write = True # opt in on 2.x; always on in 3.0
df = pd.DataFrame({"x": [1, -2, 3], "y": [0, 0, 0]})
subset = df[df["x"] > 0]
subset.loc[:, "y"] = 1 # modifies subset only
print(df["y"].tolist()) # [0, 0, 0]
col = df["x"]
col.iloc[0] = 100 # copy happens lazily here
print(df["x"].tolist()) # [1, -2, 3]
df[df["x"] > 0]["y"] = 5 # chained: warns, has no effect
print(df["y"].tolist()) # [0, 0, 0]
df.loc[df["x"] > 0, "y"] = 5 # the correct way
print(df["y"].tolist()) # [5, 0, 5]
arr = df["x"].to_numpy()
print(arr.flags.writeable) # False: shares memory with dfRemember
- Under CoW every derived object behaves as an independent copy
- Copies are lazy: memory is shared until something is written
- Chained assignment never updates the original; use one
.loc[...] = - Default in pandas 3.0; opt in on 2.2 with
pd.options.mode.copy_on_write
Watch out for
- Calling
df['col'].replace(..., inplace=True)and expectingdfto change under CoW.