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 is telling you that it detected two separate indexing operations chained together, and the assignment may have landed on a temporary intermediate object that was immediately discarded — not on your original DataFrame. In other words, your data might be completely unchanged while your code gave you no error.
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.
The practical consequence: after the chained assignment above, df is
completely unmodified. Inspect it and you will still see the original temperature
values. This is a silent data bug that can propagate through a pipeline unnoticed.
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
The general pattern: whenever you filter rows first and then want to derive a new
column value using a Series method, build the boolean mask as a variable, and use
it inside .loc[mask, col] on both the left-hand and right-hand sides.
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
There are narrow legitimate uses for suppression:
.loc[]
over multiple sprints. Silencing prevents noise while the migration is in progress.
val = df[df["x"] > 0]["y"].mean() is a read — no assignment, no
risk. (Even so, prefer explicit .loc[] for clarity.)
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.