Skip to content

Fix DataFrame accessor mutating original on assignment - #1008

Open
mborodii-prog wants to merge 1 commit into
mainfrom
873-fix-dataframe-accessor-mutates-original
Open

Fix DataFrame accessor mutating original on assignment#1008
mborodii-prog wants to merge 1 commit into
mainfrom
873-fix-dataframe-accessor-mutates-original

Conversation

@mborodii-prog

Copy link
Copy Markdown
Contributor

Fix: DataFrame accessor no longer mutates the original on assignment

Problem

When using the .wrangles accessor and assigning the result to a new variable, the original DataFrame was also mutated. Both variables ended up pointing to the same in-memory object.

df = DataFrame({'col': range(20)})
df_sample = df.wrangles.select.sample(rows=5)

print(len(df))        # 5  ← should still be 20
print(len(df_sample)) # 5
print(df is df_sample) # True ← same object

The same problem affected any wrangle that modifies columns:

df = DataFrame({'name': ['alice', 'bob', 'carol']})
df_upper = df.wrangles.convert.case(input='name', case='upper')

print(df['name'].tolist())       # ['ALICE', 'BOB', 'CAROL'] ← should be unchanged
print(df_upper['name'].tolist()) # ['ALICE', 'BOB', 'CAROL']
print(df is df_upper)            # True

Root cause

Every make_method closure in dataframe.py used this pattern:

def method(self, *args, **kwargs):
    self._df.__init__(target_func(self._df, *args, **kwargs))  # reinitialises df in-place
    return self._df                                             # returns the same object as df

Two things go wrong:

  1. self._df.__init__(result) calls __init__ on the existing object, which reinitialises its data in-place without changing its identity (id()). So df is mutated.
  2. return self._df hands back the same reference as df. Any assignment (df_sample = ...) just creates a second name for the same object.

Many wrangle functions also assign columns directly (df[col] = ...), so even without the __init__ trick the original would be modified.

The pattern appeared in four places: _wrangles_accessor.make_method, _wrangles.make_method, _read.make_method, and _read.file.

Fix

Pass a copy of the DataFrame into target_func and return its result directly — no __init__, no returning self._df:

# Before
def method(self, *args, **kwargs):
    self._df.__init__(target_func(self._df, *args, **kwargs))
    return self._df

# After
def method(self, *args, **kwargs):
    return target_func(self._df.copy(), *args, **kwargs)

Using .copy() means wrangle functions that assign columns in-place operate on a throwaway copy, so the caller's original DataFrame is never touched. The returned value is always an independent object.

Behaviour after fix

Assigning to a new variable — original unchanged:

df = DataFrame({'col': range(20)})
df_sample = df.wrangles.select.sample(rows=5)

print(len(df))         # 20 ✓
print(len(df_sample))  # 5  ✓
print(df is df_sample) # False ✓
df = DataFrame({'name': ['alice', 'bob', 'carol']})
df_upper = df.wrangles.convert.case(input='name', case='upper')

print(df['name'].tolist())       # ['alice', 'bob', 'carol'] ✓
print(df_upper['name'].tolist()) # ['ALICE', 'BOB', 'CAROL'] ✓
print(df is df_upper)            # False ✓

Reassignment still works as before:

df = DataFrame({'col': ['hello', 'world']})
df = df.wrangles.convert.case(input='col', case='upper')

print(df['col'].tolist())  # ['HELLO', 'WORLD'] ✓

Multiple calls on the same DataFrame — each independent:

df = DataFrame({'col': ['hello', 'world', 'foo', 'bar', 'baz']})
_ = df.wrangles.convert.case(input='col', case='upper')
_ = df.wrangles.select.sample(rows=2)

print(df['col'].tolist())  # ['hello', 'world', 'foo', 'bar', 'baz'] ✓
print(len(df))             # 5 ✓

ebhills commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Queue triage (2026-07-27)

  • Disposition: Ready for review
  • Delivery owner: @mborodii-prog
  • Primary reviewer: @thomasstvr
  • Next action: Review the DataFrame copy/identity behavior and regression tests.

Please keep the branch current and put the decision in GitHub. This is one of the five active review slots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dataframe function writes to existing dataframe when trying to create a new df from the result

2 participants