Understand what happens
You have assumptions about how your code is working
Make them explicit
Focus on the most obvious at first
Divide and conquer
Use documentation and architecture diagrams
But don't trust them
Make some hypotheses about what is happening during the bug
Follow the flow
At least one of your assumptions is wrong
Experiments:
What is actually happening when you run your code
Experiments are used to test your hypotheses
Be systematic
Trust nothing
Check everything
Inspect values at interfaces.
Focus on data, not the code flow.
It's all about putting your theory to the test.
Scientists use a research log
Debugging log is great to write incident retrospectives
Let's apply this approach to a toy example.
I iterate on cumulative lists:
[0], [0, 1], [0, 1, 2], ...
I add a special value at the beginning of each list: -100
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
for i in range(5):
zero_to_i = list(range(i))
l = prepend(zero_to_i)
print(len(l))
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
for i in range(5):
zero_to_i = list(range(i))
l = prepend(zero_to_i)
print(len(l))
i = 4
zero_to_i = [0, 1, 2, 3]
l = [-100, 0, 1, 2, 3]
Expected: 5
Observed: 11
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
print(prepend([0, 1, 2]))
prepend is adding -100 in front of the list.
[-100, 0, 1, 2]
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
for i in range(5):
zero_to_i = list(range(i))
print(zero_to_i)
l = prepend(zero_to_i)
Let's check the value of zero_to_i
[]
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
for i in range(5):
zero_to_i = list(range(i))
l = prepend(zero_to_i)
print(l)
Let's check the value of l
[-100]
[-100, 0]
[-100, 0, 0, 1]
[-100, 0, 0, 1, 0, 1, 2]
[-100, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3]
def prepend(l: list, beginning: list = [-100]):
beginning.extend(l)
return beginning
print(prepend([0, 1, 2]))
print(prepend([0, 1, 2]))
prepend is always behaving in the same way.
1st print: [-100, 0, 1, 2]
2nd print: [-100, 0, 1, 2, 0, 1, 2]
def prepend(l: list, beginning: list = [-100]):
print("l: ", l)
print("beginning: ", beginning)
beginning.extend(l)
return beginning
prepend([0, 1, 2])
prepend([0, 1, 2])
The default argument beginning is always [-100].
l: [0, 1, 2]
beginning: [-100]
l: [0, 1, 2]
beginning: [-100, 0, 1, 2]
def prepend(l: list, beginning: list | None = None):
if beginning is None:
beginning = [-100]
beginning.extend(l)
return beginning
for i in range(5):
zero_to_i = list(range(i))
l = prepend(zero_to_i)
Qualify the bug
Leverage temporality
Identify sources of randomness
Invest in tooling
Enroll another adventurer
Architect your maze better