Fix: pandas SettingWithCopyWarning — A value is trying to be set on a copy
Quick Answer
How to fix pandas SettingWithCopyWarning — understanding chained indexing, using .loc correctly, Copy-on-Write in pandas 2.x, and when the warning indicates a real bug vs a false alarm.
The Error
pandas raises a SettingWithCopyWarning when you try to modify a DataFrame:
/usr/local/lib/python3.11/site-packages/pandas/core/indexing.py:965: 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
self._setitem_with_indexer(indexer, value)Or a silent bug where your modification doesn’t affect the original DataFrame at all — no warning, just wrong results.
Why This Happens
pandas operations can return either a view (a reference to the original data) or a copy (a new DataFrame with duplicated data). The behavior depends on the operation and isn’t always predictable.
Chained indexing — two consecutive bracket operations — is the core issue:
df[df['age'] > 18]['salary'] = 50000
# Equivalent to:
temp = df[df['age'] > 18] # Step 1: might return a copy
temp['salary'] = 50000 # Step 2: modifies the copy, not dfStep 1 may return a copy of the data. Modifying the copy in Step 2 doesn’t affect the original df. pandas warns you about this ambiguity.
Other causes:
- Slicing without
.copy()—subset = df[100:200]may return a view or copy depending on the context. - Boolean indexing —
df[mask]typically returns a copy. - Column selection —
df[['col1', 'col2']](list of columns) returns a copy;df['col1'](single column) may return a view.
Fix 1: Use .loc for Setting Values
Replace chained indexing with a single .loc operation. .loc always modifies in place on the original DataFrame:
# WRONG — chained indexing, triggers warning
df[df['age'] > 18]['salary'] = 50000
# CORRECT — single .loc operation
df.loc[df['age'] > 18, 'salary'] = 50000More examples:
# WRONG
df[df['status'] == 'active']['score'] = df[df['status'] == 'active']['score'] * 1.1
# CORRECT
mask = df['status'] == 'active'
df.loc[mask, 'score'] = df.loc[mask, 'score'] * 1.1Setting a single cell:
# WRONG — chained indexing
df[df['id'] == 42]['name'] = 'Alice'
# CORRECT
df.loc[df['id'] == 42, 'name'] = 'Alice'
# Or by row position and column position
df.iloc[5, df.columns.get_loc('name')] = 'Alice'Why
.locworks:.loc[row_indexer, col_indexer]is a single indexing operation on the original DataFrame. There’s no intermediate copy. pandas knows unambiguously that you want to modifydfitself.
Fix 2: Call .copy() When You Intend to Work on a Subset
When you genuinely want a separate copy to modify without affecting the original, be explicit:
# Without .copy() — might be a view, triggers warning when you modify it
young_users = df[df['age'] < 25]
young_users['discount'] = 0.2 # Warning — are we modifying df or a copy?
# With .copy() — explicit copy, no ambiguity, no warning
young_users = df[df['age'] < 25].copy()
young_users['discount'] = 0.2 # No warning — working on a known copyUse .copy() when:
- You’re creating a subset to work with independently
- You want to add/modify columns without affecting the original
- You’re passing a subset to a function that modifies it
Don’t use .copy() when:
- You want your modifications to reflect back to the original (use
.locinstead)
Fix 3: Understand pandas 2.0 Copy-on-Write
pandas 2.0 introduced Copy-on-Write (CoW), which changes the behavior significantly. In pandas 2.x with CoW enabled, indexing operations always return copies — but they’re lazy copies (only duplicated when modified). This eliminates the ambiguity:
# Enable CoW in pandas 1.5+ (enabled by default in pandas 3.0)
import pandas as pd
pd.options.mode.copy_on_write = True
df = pd.DataFrame({'age': [20, 30, 40], 'salary': [50000, 60000, 70000]})
# With CoW, this no longer triggers a warning:
subset = df[df['age'] > 25]
subset['salary'] = 99999 # Modifies the copy, not df — clear and unambiguous
print(df['salary']) # [50000, 60000, 70000] — df unchangedCoW changes the semantics: modifications to subsets never affect the original. If you want to modify the original, you must use .loc explicitly:
# With CoW — modify the original using .loc
df.loc[df['age'] > 25, 'salary'] = 99999 # ← Required to affect dfCheck your pandas version:
import pandas as pd
print(pd.__version__)
# 2.0+ has CoW available
# 3.0+ CoW is the defaultFix 4: Use .assign() for Method Chaining
.assign() always returns a new DataFrame with the modification applied, making chaining safe and explicit:
# Instead of modifying in place with chained indexing
df_active = df[df['status'] == 'active']
df_active['score'] = df_active['score'] * 1.1 # Warning
# Use .assign() — returns a new DataFrame, no mutation
df_active = (
df[df['status'] == 'active']
.assign(score=lambda x: x['score'] * 1.1)
.assign(level=lambda x: pd.cut(x['score'], bins=[0, 50, 100], labels=['low', 'high']))
).assign() is particularly clean for data transformation pipelines where you’re building a new DataFrame rather than modifying an existing one.
Fix 5: Silence or Upgrade the Warning Appropriately
If the warning is a false positive (you’ve verified your code is correct), suppress it for a specific block:
import pandas as pd
import warnings
# Suppress for a specific operation you've verified is correct
with warnings.catch_warnings():
warnings.simplefilter("ignore", pd.errors.SettingWithCopyWarning)
df['new_col'] = 'value' # You know this is safeGlobally disable the warning (use sparingly — you may miss real bugs):
pd.options.mode.chained_assignment = None # Suppress completely
# or
pd.options.mode.chained_assignment = 'warn' # Default — show warning
# or
pd.options.mode.chained_assignment = 'raise' # Convert to error (strictest)Warning: Setting
chained_assignment = Noneglobally hides warnings that might indicate real bugs — modifications that look like they work but silently fail. Use this only when you’ve explicitly verified the code is correct and want to reduce noise.
Prefer upgrading the code over silencing. The warning exists because chained assignment is genuinely ambiguous and error-prone. Fix the code with .loc or .copy() rather than suppressing the warning.
Fix 6: Diagnose Whether You Have a Real Bug
The warning doesn’t always mean your code is wrong — sometimes it’s a false positive. Check whether your intended modification actually took effect:
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# This might or might not work — depends on whether df[...] returned a view or copy
df[df['a'] > 1]['b'] = 99
# Verify if df actually changed
print(df)
# a b
# 0 1 4
# 1 2 5 ← Still 5, not 99? You have a real bug — use .loc
# 2 3 6If df didn’t change, you were modifying a copy — that’s a bug. Use .loc:
df.loc[df['a'] > 1, 'b'] = 99
print(df)
# a b
# 0 1 4
# 1 2 99 ✓
# 2 3 99 ✓Common Patterns and Their Fixes
# Pattern 1: Modify a filtered subset
# WRONG
df[df['country'] == 'US']['tax_rate'] = 0.25
# CORRECT
df.loc[df['country'] == 'US', 'tax_rate'] = 0.25
# Pattern 2: Work on a subset independently
# WRONG (ambiguous)
us_users = df[df['country'] == 'US']
us_users['category'] = 'domestic' # Warning
# CORRECT (explicit copy)
us_users = df[df['country'] == 'US'].copy()
us_users['category'] = 'domestic' # No warning
# Pattern 3: Apply transformation to a column in filtered rows
# WRONG
df[df['score'] > 0]['normalized'] = df[df['score'] > 0]['score'] / 100
# CORRECT
mask = df['score'] > 0
df.loc[mask, 'normalized'] = df.loc[mask, 'score'] / 100
# Pattern 4: In a function that receives a DataFrame slice
def process_subset(subset):
# WRONG — modifies 'subset', but caller may get a copy
subset['processed'] = True
# CORRECT — work on an explicit copy
subset = subset.copy()
subset['processed'] = True
return subset
# Pattern 5: Apply row by row (avoid when possible — slow)
# If you must use apply, return a new Series rather than mutating
df['new_col'] = df.apply(lambda row: compute(row['a'], row['b']), axis=1)Still Not Working?
Enable strict mode to catch bugs earlier:
pd.options.mode.chained_assignment = 'raise'
# SettingWithCopyWarning becomes an exception — easy to find the exact lineInspect whether an object is a view or copy:
# Check if two DataFrames share memory
import numpy as np
print(np.shares_memory(df['col'], subset['col']))
# True = view (shared memory)
# False = copy (independent memory)Check pandas documentation for your operation. The pandas docs include a table of which operations return views vs copies. The rules changed in pandas 1.x and again in 2.x.
For related pandas issues, see Fix: pandas merge KeyError and Fix: Python TypeError Unhashable Type List.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pandas merge() Key Error and Duplicate Columns (_x, _y)
How to fix pandas merge and join errors — KeyError on merge key, duplicate _x/_y columns, unexpected row counts, suffixes, and how to validate merge results.
Fix: Jupyter Notebook Not Working — Kernel Dead, Module Not Found, and Widget Errors
How to fix Jupyter errors — kernel fails to start or dies, ModuleNotFoundError despite pip install, matplotlib plots not showing, ipywidgets not rendering in JupyterLab, port already in use, and jupyter command not found.
Fix: LightGBM Not Working — Installation Errors, Categorical Features, and Training Issues
How to fix LightGBM errors — ImportError libomp libgomp not found, do not support special JSON characters in feature name, categorical feature index out of range, num_leaves vs max_depth overfitting, early stopping callback changes, and GPU build errors.
Fix: Matplotlib Not Working — Plots Not Showing, Blank Output, and Figure Layout Problems
How to fix Matplotlib errors — plot not displaying, blank figure, RuntimeError main thread not in main loop, tight_layout UserWarning, overlapping subplots, savefig saving blank image, backend errors, and figure/axes confusion.