It's not a bug, it's a Mental Model Impedance Mismatch¶
Introduction¶
I once spent the best part of a few days (many years ago) chasing a bug in a FORTRAN program of mine (I did say it was many years ago!).
FORTRAN tried to support the mental model of mathematicians, and variables starting with I, K, L, M, N defaulted to INTEGER type. I had a variable representing the rest mass of the electron (0.511 MeV), and so I had the line:
mc2 = 0.511
Of course, the 0.511 was truncated to 0 . My high energy mental model didn't mesh with FORTRAN's type model.
Well, I recently ran across two examples of mental model mismatch in Python, which I will explain below. Python has a few areas like this. For example, default function parameters are initialized once only, and are mutable: I initially expected them to be re-initialized on each function call.
Implementation¶
%load_ext lab_black
%load_ext watermark
import copy
Discussion¶
Dictionary copy¶
So I had a dictionary, where the data item attached to each Key was a list. The dictionary represented the state of a real world object, and I had a function that changed the state of the object (altered some list values for some Keys). I wanted to compare the Before and After state of the object.
First the setup¶
Set up the initial state
my_dict = {"A": [1, 2, 3], "B": [4, 5, 6]}
Copy the object (that we will modify)
my_dict_copy = my_dict.copy()
Modify some of the attributes of the object
my_dict_copy["A"][1] = 9
And now the pratfall¶
When we display the original object, we find that it has been modified, just by the act of modifying the object created by a copy
my_dict
{'A': [1, 9, 3], 'B': [4, 5, 6]}
my_dict_copy
{'A': [1, 9, 3], 'B': [4, 5, 6]}
A Copy isn't a Copy¶
Again, my mental model was that a copy()
would create an entirely independent object. Not So! In fact (as shown below) I should use the deepcopy
function from the copy
module (that I had never head of before this bug). I can understand the rationale behind this language design decision, but it would have been more transparent of the dict
copy()
method was named shallowcopy()
. But too late now, I guess.
A DeepCopy is a Copy¶
We repeat the code above, and show that modifying the DeepCopy does indeed NOT change the original.
Create a DeepCopy, and modify it.
my_dict_copy = copy.deepcopy(my_dict)
my_dict_copy["A"][1] = 77
my_dict_copy
{'A': [1, 77, 3], 'B': [4, 5, 6]}
Original is unchanged
my_dict
{'A': [1, 9, 3], 'B': [4, 5, 6]}
Function lookup¶
This bug arose partially because I was working in a Jupyter Notebook, and jumping around like a demented jackrabbit, changing cells (modifying a function definition), and then jumping down to run a piece of code again (poor discipline, I know).
However, it can be demonstrated by straight line code
Define a function¶
def f1():
print("f1 version 1")
# end f1
Run the function and get its ID
f1()
id(f1)
f1 version 1
2024043903744
Now define a lookup table that takes a Key, and returns a reference to the function
op_table = {"X": f1}
Test the lookup table works (it does), and show the ID of the function it references. As expect, the same as the original function ID.
op_table["X"]()
id(op_table["X"])
f1 version 1
2024043903744
Define a new version of the function¶
def f1():
print("f1 version 2")
# end f1
Test it works (it does)
f1()
f1 version 2
Run the lookup table. We get the old version of the function!
My initial mental model was that Python would remember the name of the function (here f1
), and always adjust some magic pointers to always point to the latest version. After all, this is what Python does when I type in the function name at the Python prompt - it ignores my earlier definition in favour of the latest.
Printing out the ID shows that the lookup ID value is unchanged by the re-definition of the function.
*Whenever we change or re-define a function, we need to repeat the creation of the lookup table that references the function.*
op_table["X"]()
f1 version 1
Show the ID of the function in the lookup table.. The same as the ID of the original function definition
id(op_table["X"])
2024043903744
Show the ID of the newly re-defined function
id(f1)
2024044036160
Reproducability¶
%watermark
Last updated: 2025-08-03T18:58:30.206203+10:00 Python implementation: CPython Python version : 3.11.7 IPython version : 8.20.0 Compiler : MSC v.1916 64 bit (AMD64) OS : Windows Release : 10 Machine : AMD64 Processor : Intel64 Family 6 Model 170 Stepping 4, GenuineIntel CPU cores : 22 Architecture: 64bit
%watermark -h -iv -co
conda environment: base Hostname: INSPIRON16 json : 2.0.9 sys : 3.11.7 | packaged by Anaconda, Inc. | (main, Dec 15 2023, 18:05:47) [MSC v.1916 64 bit (AMD64)] numpy : 1.26.4 xarray : 2023.6.0 pandas : 2.1.4 ipywidgets: 7.6.5