← Home

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.

Pale blue cubes falling straight through an empty transparent grid and piling up on the floor below, the grid completely unchanged.

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

Same code, pandas 2 and pandas 3
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 dtype

    Downcasting 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.