The List That Changed Because Two Variables Held the Same Array
The helper sorted the array in place and returned it, so the caller's original was reordered too, and the second consumer saw a list nobody had touched, and…
Start at the wrong and you miss it. Start at the incident and you miss how it arrived. The bug lived in the hand-off.
Strip the in-place mutation through an alias down and one property makes it hard to trace: the mutation happens in a function that looks pure, and the effect appears in a variable that was never passed to it, so the search for the writer looks in the wrong place.
This was Python, and the mechanics are the same for any language where assignment copies the reference rather than the value.
Why the original changed
The helper took the list, and the list was the parameter, and the parameter was the reference, and the reference was the same object, and the same object was the caller's, and the caller's was the sorted, and the sorted was the in place, and the in place was the no copy, and the no copy was the mutation's visibility, and the visibility was the caller's surprise.
The assignment was the binding, and the binding was the not the copy, and the not copy was the alias, and the alias was the two names, and the two names were the one object, and the one object was the mutation's reach, and the reach was the incident, and the incident is the reason the assignment's semantics matter.
The return was the same list, and the same list was the convenience, and the convenience was the chain, and the chain was the readable, and the readable was the appeal, and the appeal was the trap, because the returned list looks like the new value and is the mutated original, and the mutated original is the other consumer's surprise.
Why the search failed
The search was the writer, and the writer was the report's code, and the code was the read, and the read was the innocent, and the innocent was the confusion, because the report never mutated the list, and the never was the true, and the true was the misleading, because the mutation was the upstream helper, and the helper was the other function, and the other function was the not searched.
The trace was the needed, and the trace was the identity, and the identity was the id, and the id was the same for both variables, and the same was the proof, and the proof was the alias, and the alias was the answer, and the answer is the reason the identity check is the diagnosis for the mysterious mutation.
This is the same family as the mutable default argument that accumulated across calls, same mechanism, different costume.
What I changed
Made the helper return a copy. The sort became the sorted on the copy, and the copy was the caller's original's safety, and the safety was the fix, and the fix was the one line, and the line was the slice or the list call, and the call was the discipline, because the helper that looks pure should be pure.
Named the mutating functions distinctly. The in-place version was the sort_underscore or the docstring's warning, and the warning was the review's catch, and the catch was the fix, and the fix was the convention, and the convention was the discipline, because the name should say whether the argument survives.
Froze the shared data where possible. The tuple was the immutable, and the immutable was the mutation's error, and the error was the loud, and the loud was the fix, and the fix was the type, and the type was the discipline, because the immutable data cannot be aliased into a surprise.
Added a test that asserts the input is unchanged. The test called the helper and compared the original, and the compared was the unchanged, and the unchanged was the assertion, and the assertion was the guard, and the guard was the prevention, and the prevention was the fix, and the fix was the one test that would have caught the mutation on the first commit.
Checked the identity when debugging the mystery. The id comparison was the alias's proof, and the proof was the diagnosis, and the diagnosis was the fix's entry, and the entry was the discipline, because the mysterious mutation is the shared reference until proven otherwise.
What I now do
Assignment binds a reference rather than copying the value, so a function that mutates its argument changes the caller's object, and a helper that sorts in place and returns the list looks pure while aliasing. Copy before mutating, name the mutating functions distinctly, prefer immutable types for shared data, and assert in a test that the input is unchanged.
The report showed a sorted list that nobody in the report's code had sorted, because a helper three calls upstream sorted the same array object in place and returned it, and the two variables were one list. That is the whole pattern, and it is the part worth remembering.
Related reading: The Enum Case Nobody Handled and the Compiler Did Not Catch explores a connected angle.
Related reading: The Loop That Deleted From the Dict It Was Reading explores a connected angle.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud