The pandas 3 Change That Will Break Your Code Does Not Warn You
Everyone is fixing SettingWithCopyWarning. That one never worked anyway. The write that works today and silently stops working under Copy-on-Write does not emit a warning in either version.

The pandas 3 migration advice you will find is mostly about SettingWithCopyWarning.
That is the wrong thing to be worried about, and it is worth being precise about why:
the code that warning fires on never worked. Fixing it changes nothing about your
results.
The code that did work, and stops, is silent in both directions.
The one that matters
Measured on pandas 2.3.3 (Python 3.9.6):
df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
col = df["a"]
col.iloc[0] = 99
df["a"].tolist()
| result | warnings | |
|---|---|---|
| pandas 2 default | [99, 2, 3] |
none |
| pandas 3 default (Copy-on-Write) | [1, 2, 3] |
none |
The value goes from 99 to 1. Neither version says a single word about it.
That is the whole problem. Running your test suite with -W error::FutureWarning —
the standard advice for a pandas migration — will not surface this, because there is
no warning to raise.
Compare them yourself
col = df["a"] col.iloc[0] = 99 df["a"].tolist()
pandas 2 default
result [99, 2, 3]
warnings
none
pandas 3 default
result [1, 2, 3]
warnings
none
This is the one to worry about. The result changes and neither version says anything, so running your tests with -W error::FutureWarning will not find it.
The ones a warning filter does find
These all emit a warning today, so one run with warnings turned into errors finds every occurrence. They are the easy half.
df.applymap(fn)DataFrame.applymap has been deprecated. Use DataFrame.map instead.
s.fillna(method='ffill')Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.
s.replace({1: 1.5}) # object dtypeDowncasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
df.groupby("g").apply(fn)DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
Measured on pandas 2.3.3 / Python 3.9.6 on 2026-08-07.
Six patterns, each run under both defaults, results and warnings recorded verbatim. Of the six, exactly one changes result with no warning on either side.
Why the famous warning is the wrong target
Chained assignment produces the same result under both defaults:
df[df.a > 1]["b"] = 0
# pandas 2: [10, 20, 30] SettingWithCopyWarning
# pandas 3: [10, 20, 30] ChainedAssignmentError
It did not update the DataFrame before, and it does not now. What improved is the honesty of the message. pandas 2 says the object may be a copy; Copy-on-Write says it never works, because the intermediate object always behaves as a copy.
That is a real upgrade — a maybe became a guarantee — but it is a documentation improvement, not a behaviour change. Your results were already whatever they were.
The inplace case is halfway
df["a"].fillna(0, inplace=True)
This one does change result — [1.0, 0.0, 3.0] becomes [1.0, nan, 3.0] — but
pandas 2 warns about it, and the warning finishes the thought:
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.
It even hands you the fix: df.method({col: value}, inplace=True). So this one is
findable before you upgrade. Good.
What not to change
Two of the six are stable, and knowing that saves work:
df.loc[df.a > 1, "b"] = 0 # identical under both, no warnings
renamed = df.rename(...) # already returned a copy; nothing changes
The single-step .loc form is the target to convert to. It is not a workaround —
it is the form whose behaviour does not depend on which pandas you are running.
How to actually find these
Not with a warning filter. With the flag:
pd.options.mode.copy_on_write = True
That option in pandas 2 is the pandas 3 behaviour. Turn it on, run your test suite, and anything that fails is something the upgrade would have broken later, quietly, in production. It costs one line and one test run.
The deprecation warnings still deserve a pass — applymap, fillna(method=...), the
replace downcasting warning, groupby().apply() on grouping columns — and those are
genuinely easy: they announce themselves, so one run with warnings raised as errors
finds every occurrence.
But they are the half you were already going to find. Spend the afternoon on the half that does not tell you.
Common follow-ups
Is SettingWithCopyWarning the thing to fix before pandas 3?
It is the loudest thing, but not the dangerous one. Measured on pandas 2.3.3, chained assignment produces the same result under both defaults — it did not update the DataFrame before either. What changes is the message, which goes from "may be a copy" to a statement that it never works.
Which pattern actually changes result?
Binding an intermediate object and then writing to it. col = df["a"] followed by col.iloc[0] = 99 updates the original on pandas 2 and does not under Copy-on-Write. The equivalent with an .iloc slice changes too, though pandas 2 at least warns about that one.
How do I find these before upgrading?
Set pd.options.mode.copy_on_write = True and run your test suite. That option in pandas 2 is the pandas 3 behaviour, so anything that breaks under it is something the upgrade would have broken later. A warning filter cannot substitute, because the pattern that matters emits no warning.
What is the safe way to write?
A single-step .loc assignment. df.loc[df.a > 1, "b"] = 0 produced identical results under both defaults with no warnings in either, which is what makes it worth converting to.
Do all my deprecation warnings matter then?
They matter, but they are the easy half — applymap, fillna(method=...), the replace downcasting warning and groupby().apply() on grouping columns all announce themselves, so one run with warnings raised as errors finds every occurrence.