Skip to content

Fix: pandas SettingWithCopyWarning — A value is trying to be set on a copy

FixDevs ·

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 df

Step 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 indexingdf[mask] typically returns a copy.
  • Column selectiondf[['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'] = 50000

More 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.1

Setting 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 .loc works: .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 modify df itself.

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 copy

Use .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 .loc instead)

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 unchanged

CoW 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 df

Check your pandas version:

import pandas as pd
print(pd.__version__)
# 2.0+ has CoW available
# 3.0+ CoW is the default

Fix 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 safe

Globally 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 = None globally 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  6

If 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 line

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

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles