Question: While copying a pandas dataframe, ideally, we should use .copy(), which is a deep copy by default. We could also achieve the same using new_df = pd.Dataframe(old_df), which is also intuitive (and common style across most programming languages since in principle, we are calling a copy constructor). In both cases, they have different memory identifers as shown below. But when I change the old_df using .iloc, it changes the value for the new_df. Is this the expected behaviour? I couldn't figure out this behaviour using the docs. Am I missing something trivial? # ! pip install smartprint from smartprint import smartprint as sprint import pandas as pd data = {'A': [1, 2, 3], 'B': [4, 5, 6]} original_df = pd.DataFrame(data) new_df = pd.DataFrame(original_df) original_df['A'] = original_df['A'].apply(lambda x: x * 1000) print ("============ Changing a column with pd.Dataframe(old_df); No change") sprint(original_df) sprint(new_df) sprint (id(new_df), id(original_df)) data = {'A': [1, 2, 3], 'B': [4, 5, 6]} original_df = pd.DataFrame(data) new_df = pd.DataFrame(original_df) original_df.iloc[0,:] = 20 print ("============ Using .iloc with pd.Dataframe(old_df); Change") sprint(original_df) sprint(new_df) sprint (id(new_df), id(original_df)) data = {'A': [1, 2, 3], 'B': [4, 5, 6]} original_df = pd.DataFrame(data) new_df = original_df.copy() original_df.iloc[0,:] = 20 print ("============ Using .iloc with old_df.copy(); No change") sprint(original_df) sprint(new_df) sprint (id(new_df), id(original_df))  Output: ============ Changing a column with pd.Dataframe(old_df); No change original_df : A B 0 1000 4 1 2000 5 2 3000 6 new_df : A B 0 1 4 1 2 5 2 3 6 id(new_df), id(original_df) : 140529132556544 140529514510944 ============ Using .iloc with pd.Dataframe(old_df); Change original_df : A B 0 20 20 1 2 5 2 3 6 new_df : A B 0 20 20 1 2 5 2 3 6 id(new_df), id(original_df) : 140528893052000 140528894065584 ============ Using .iloc with old_df.copy(); No change original_df : A B 0 20 20 1 2 5 2 3 6 new_df : A B 0 1 4 1 2 5 2 3 6 id(new_df), id(original_df) : 140529057828336 140528940223984  My python and pandas versions are listed below: import sys sys.version Out[16]: '3.8.17 (default, Jul 5 2023, 16:18:40) \n[Clang 14.0.6 ]' pd.__version__ Out[17]: '1.4.3' 

Answer: This is the expected behavior of DataFrame, it is also happening when you're passing a numpy array as input: a = np.array([[1,2],[3,4]]) df = pd.DataFrame(a) a[0][0] = 9 print(df) 0 1 0 99 2 1 3 4  It is actually well described in the DataFrame documentation:  copy: bool or None, default None Copy data from inputs. For dict data, the default of None behaves like copy=True. For DataFrame or 2d ndarray input, the default of None behaves like copy=False. If data is a dict containing one or more Series (possibly of different dtypes), copy=False will ensure that these inputs are not copied.  a = np.array([[1,2],[3,4]]) df = pd.DataFrame(a, copy=True) a[0][0] = 99 print(df) 0 1 0 1 2 1 3 4  what about changing a full column? In this case you don't mutate the column but overwrite it with a new one. This means that the underlying objects are now different: a = np.array([[1,2],[3,4]]) df = pd.DataFrame(a) df[0] = 99 print(a) [[1 2] [3 4]]