Fix pandas SettingWithCopyWarning: Chained Assignment and .loc Solutions

Fix pandas SettingWithCopyWarning: Chained Assignment and .loc Solutions

pandas | Data Engineering | July 2026

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.

What the warning actually says

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.

Why it is a warning and not an error: pandas cannot always determine at runtime whether chained indexing produced a view or a copy — it depends on the internal memory layout (C-contiguous vs. non-contiguous). The behavior is therefore undefined and implementation-dependent, which is precisely why the warning exists: to tell you that you are in undefined territory.

Why chained indexing creates a copy in the first place

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 fix that covers most cases: .loc[]

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.

More .loc[] patterns

# 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

When you actually want an independent subset: .copy()

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.

Rule of thumb: If you will read from the subset, you may not need .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 and .apply() version of the same mistake

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 mostly makes this moot: Copy-on-Write

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

Can you just silence it? Rarely, and carefully

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
This is dangerous if misused. Silencing the warning does not fix the underlying problem — your chained assignments may still silently fail to modify the DataFrame you intended. Suppression is only appropriate when you have carefully audited all your indexing code and confirmed there are no actual chained assignment bugs in the scope where it is applied.

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().

Quick Reference: Chained Assignment Modes

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.

Related pandas fixes