Few warnings trip up pandas beginners and intermediate users alike as reliably as
SettingWithCopyWarning. It appears after what looks like a perfectly
reasonable assignment, it is non-fatal, and yet it signals that your data may not
have been modified at all. This guide explains exactly what the warning means, why
pandas raises it, and every practical fix — from the one-line .loc[]
replacement to the Copy-on-Write mode introduced in pandas 2.0.
When you run code like the following:
import pandas as pd
df = pd.DataFrame({
"city": ["Madrid", "Berlin", "Tokyo", "Paris"],
"temperature": [28, 15, 32, 21],
"country": ["Spain", "Germany", "Japan", "France"],
})
# Chained indexing — common mistake
df[df["temperature"] > 20]["temperature"] = 99
pandas prints a warning similar to this:
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation:
https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
The key phrase is "on a copy of a slice". pandas detected two indexing operations chained together, and the assignment may have landed on a temporary object that got discarded immediately after, not your original DataFrame. Nothing raises, nothing crashes, and the values you thought you set are still sitting there unchanged.
Pandas indexing can return either a view (a reference into the same underlying NumPy array) or a copy (a brand-new array with duplicated data). When you chain two indexing operations, the first one may already produce a copy, so the second one — the assignment — writes into that copy, not the original.
# This line is actually two operations:
df[df["temperature"] > 20]["temperature"] = 99
# pandas sees it as:
temp = df[df["temperature"] > 20] # Operation 1: might return a copy
temp["temperature"] = 99 # Operation 2: writes to temp, not df
The internal rules for when pandas returns a view versus a copy are complex and not part of the public API contract. Fancy indexing (boolean masks, lists of labels) almost always returns a copy. Basic integer or label slicing sometimes returns a view. You cannot reliably predict which path will be taken, so you should never write code that depends on either outcome.
Inspect df after that chained assignment and the temperature values
are exactly what they were before: a silent data bug, the kind that propagates
three steps downstream before anyone notices the numbers are wrong.
The canonical fix for the vast majority of SettingWithCopyWarning
cases is to collapse two indexing operations into a single .loc[] call.
.loc[] always operates directly on the original DataFrame and never
produces an intermediate copy.
import pandas as pd
df = pd.DataFrame({
"city": ["Madrid", "Berlin", "Tokyo", "Paris"],
"temperature": [28, 15, 32, 21],
"country": ["Spain", "Germany", "Japan", "France"],
})
# BEFORE (raises SettingWithCopyWarning, may not work):
df[df["temperature"] > 20]["temperature"] = 99
# AFTER (correct, no warning):
df.loc[df["temperature"] > 20, "temperature"] = 99
print(df)
# city temperature country
# 0 Madrid 99 Spain
# 2 Tokyo 99 Japan
# 3 Paris 99 France
# 1 Berlin 15 Germany
The syntax is df.loc[row_selector, column_name]. The row selector can
be a boolean Series, a list of labels, a slice, or a scalar label. The column name
is a single label or a list of labels. This single expression goes directly into the
underlying array without any intermediate object.
# Set multiple columns at once
df.loc[df["temperature"] > 20, ["temperature", "country"]] = [99, "Hot"]
# Conditional update using another column's value
df.loc[df["country"] == "Japan", "temperature"] = df.loc[df["country"] == "Japan", "temperature"] * 1.1
# Reset a value to NaN
import numpy as np
df.loc[df["city"] == "Berlin", "temperature"] = np.nan
# Using index positions instead of labels — use .iloc for that
df.iloc[0, 1] = 30 # row 0, column index 1
Sometimes you deliberately want to extract a subset of a DataFrame and modify it
independently, without affecting the original. In that case, the fix is to call
.copy() explicitly when creating the subset.
import pandas as pd
df = pd.DataFrame({
"city": ["Madrid", "Berlin", "Tokyo", "Paris"],
"temperature": [28, 15, 32, 21],
"country": ["Spain", "Germany", "Japan", "France"],
})
# WRONG — subset may be a view, modifying it is undefined behavior:
hot_cities = df[df["temperature"] > 20]
hot_cities["label"] = "hot" # SettingWithCopyWarning here
# CORRECT — explicitly copy so you own the data:
hot_cities = df[df["temperature"] > 20].copy()
hot_cities["label"] = "hot" # No warning, works as expected
print(hot_cities)
# city temperature country label
# 0 Madrid 28 Spain hot
# 2 Tokyo 32 Japan hot
# 3 Paris 21 France hot
Use .copy() whenever your intent is to produce a derived DataFrame
that has its own independent data. This is the correct pattern for feature
engineering pipelines where you filter rows, add computed columns, and pass the
result to a model — all without touching the original DataFrame.
.copy(). If you will write to the subset (add or
modify columns/values), always call .copy() to make your intent
explicit and avoid the warning.
The .str accessor and .apply() are common sources of
chained assignment because they return a new Series, and assigning back to a
column on a slice triggers the warning.
import pandas as pd
df = pd.DataFrame({
"name": [" Alice ", "bob", "CHARLIE"],
"score": [88, 72, 95],
})
# WRONG — chained: df[mask]["name"] = ...
subset = df[df["score"] > 80]
subset["name"] = subset["name"].str.strip().str.title() # Warning
# CORRECT — use .loc[] with the mask and column name:
mask = df["score"] > 80
df.loc[mask, "name"] = df.loc[mask, "name"].str.strip().str.title()
print(df)
# name score
# 0 Alice 88
# 1 bob 72
# 2 Charlie 95
# apply() follows the same rule
import pandas as pd
df = pd.DataFrame({"value": [1, 2, 3, 4, 5]})
# WRONG — assigns result of apply to a column on a filtered copy:
df[df["value"] > 2]["value"] = df[df["value"] > 2]["value"].apply(lambda x: x ** 2)
# CORRECT:
mask = df["value"] > 2
df.loc[mask, "value"] = df.loc[mask, "value"].apply(lambda x: x ** 2)
print(df)
# value
# 0 1
# 1 2
# 2 9
# 3 16
# 4 25
Both examples share the same shape: build the boolean mask as its own variable
first, then reuse it inside .loc[mask, col] on both sides of the
assignment. Once you've written it this way a few times it stops feeling like a
workaround and just becomes how you filter-and-assign.
pandas 2.0 introduced Copy-on-Write (CoW) as an opt-in behavior
that completely changes how views and copies work. Under CoW, every indexing
operation that could return a view now returns a lazy copy that only materializes
when you modify it. This eliminates the ambiguity that causes
SettingWithCopyWarning entirely.
import pandas as pd
# Enable Copy-on-Write globally (recommended for pandas 2.x projects)
pd.options.mode.copy_on_write = True
df = pd.DataFrame({
"city": ["Madrid", "Berlin", "Tokyo"],
"temperature": [28, 15, 32],
})
# Under CoW, this no longer silently fails — it raises a ChainedAssignmentError
# or simply does not propagate back, making the bug visible:
subset = df[df["temperature"] > 20]
subset["temperature"] = 99 # Does NOT modify df
print(df["temperature"].tolist()) # [28, 15, 32] — original unchanged, clearly
# The correct pattern is still .loc[] or .copy():
df.loc[df["temperature"] > 20, "temperature"] = 99
print(df["temperature"].tolist()) # [99, 15, 99]
Copy-on-Write will become the default behavior in a future major version of pandas.
Starting new projects with pd.options.mode.copy_on_write = True is
strongly recommended because it surfaces hidden chained assignment bugs immediately
rather than silently producing wrong results.
To enable CoW for the entire session via an environment variable, set
PANDAS_COPY_ON_WRITE=1 before launching your Python process. This is
convenient for CI pipelines and Jupyter kernel configurations.
# In pandas 2.0+, you can also set it per-block using the option context manager:
with pd.option_context("mode.copy_on_write", True):
subset = df[df["temperature"] > 20].copy()
subset["temperature"] = 50
You can suppress the warning globally by setting the chained assignment option to
None:
import pandas as pd
pd.options.mode.chained_assignment = None # suppress the warning
The one case where suppression is genuinely reasonable is a phased migration on a
legacy codebase, thousands of lines of working chained indexing that you're
replacing with .loc[] gradually over several sprints, where silencing
just cuts the noise while that work is in progress. Read-only chained access
(val = df[df["x"] > 0]["y"].mean(), no assignment involved) is
technically zero-risk too, though there's little reason not to just write it as
.loc[] from the start. Outside of an active migration, if you're
reaching for suppression, it's worth asking whether you've actually verified there's
no real bug, or whether you're just making the warning go away.
A safer alternative to global suppression is to use option_context
to scope the suppression to a tight block:
import pandas as pd
# Suppress only within a known-safe block
with pd.option_context("mode.chained_assignment", None):
df["col"][df["col"].isna()] = 0 # legacy code you haven't refactored yet
Never suppress the warning at module level in library code or in shared notebooks,
because downstream users will inherit the suppression without knowing it. Always
prefer the structural fix: use .loc[] or .copy().
import pandas as pd
# Default: raises SettingWithCopyWarning
pd.options.mode.chained_assignment = "warn" # default
# Raise a hard error instead (stricter, good for CI)
pd.options.mode.chained_assignment = "raise"
# Suppress the warning entirely (use with caution)
pd.options.mode.chained_assignment = None
# pandas 2.0+: enable Copy-on-Write (best long-term fix)
pd.options.mode.copy_on_write = True
Setting chained_assignment = "raise" is a useful CI/CD strategy: it
turns the warning into a hard exception so that new chained assignment code cannot
be merged without being caught by tests.