contentintech
Learn/Fresher SDE Preparation/Debugging and code reading
Beginner~5 min read + exercises

Debugging and code reading — find the cause, prove the fix

Six reproducible bugs, corrected implementations and a practical investigation workflow.

DebuggingPythonTesting

Investigate before editing

A useful bug report contains the input, expected output, observed output and steps to reproduce. Read the traceback from its final error upward to your own code. Reduce the failing case, form one hypothesis and test it. Changing several lines at once makes it harder to know which change mattered.

For each exercise below, predict the behavior before executing it. Then write a regression test that fails on the original version and passes on the correction. That test is evidence of the fix; “it works on my sample” is weaker.

Bug 1 — negative numbers break the maximum

python
def broken_maximum(values):
    best = 0
    for value in values:
        best = max(best, value)
    return best

assert broken_maximum([-9, -4]) == 0  # Wrong; 0 is not in the input.

The hidden assumption is that some value is non-negative. Correct the initialization and define empty behavior explicitly.

python
def maximum(values):
    if not values:
        return None
    best = values[0]
    for index in range(1, len(values)):
        best = max(best, values[index])
    return best

assert maximum([-9, -4]) == -4
assert maximum([]) is None

Bug 2 — a shared default argument

python
def broken_add(value, items=[]):
    items.append(value)
    return items

assert broken_add(1) == [1]
assert broken_add(2) == [1, 2]  # The second call unexpectedly shares state.

Default arguments are evaluated once when the function is defined. Create per-call state deliberately.

python
def add(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

assert add(1) == [1]
assert add(2) == [2]

If a caller supplies a list, this implementation mutates it. Either document that or copy it before adding. A fix must preserve the intended contract, not merely satisfy a single test.

Bug 3 — skipping the last element

python
def broken_contains(values, target):
    for index in range(len(values) - 1):
        if values[index] == target:
            return True
    return False

assert broken_contains([4, 8], 8) is False

The stopping boundary is wrong. Prefer iterating values directly when you do not need positions.

python
def contains(values, target):
    return any(value == target for value in values)

assert contains([4, 8], 8)
assert contains([8], 8)
assert not contains([], 8)

Bug 4 — removing items while iterating

python
values = [0, 0, 1]
for value in values:
    if value == 0:
        values.remove(value)
assert values == [0, 1]  # A zero was skipped after positions shifted.

Build a filtered list when preserving the original object is unnecessary.

python
values = [0, 0, 1]
filtered = [value for value in values if value != 0]
assert filtered == [1]
assert values == [0, 0, 1]

For an in-place requirement, use a write-pointer approach. Link the choice to the caller's needs rather than changing mutation behavior silently.

Bug 5 — mixing a value with its position

A search function returns the index 0 for a match at the start. The condition if index: treats that successful result as false. Use an explicit sentinel such as None for no match and test index is not None. Similarly, a database id, HTTP status or count should not be judged by a generic truthiness check unless that rule is intended.

python
def first_index(values, target):
    for index, value in enumerate(values):
        if value == target:
            return index
    return None

index = first_index([7, 9], 7)
assert index == 0
assert index is not None

Bug 6 — hiding a persistence failure

A script catches every exception while loading a file and returns an empty list. A corrupted JSON file becomes “no records,” and a later save can overwrite real data. Catch only the missing-file case if a missing file legitimately means a new dataset. Let parse errors produce a clear error, and do not save until the issue is resolved.

This principle also applies to network calls: a failed request is not the same as a successful request returning no results. Model loading, empty and error states separately.

Code-reading drill

python
def accumulate(values):
    seen = set()
    result = []
    for value in values:
        if value not in seen:
            seen.add(value)
            result.append(value * 2)
    return result

assert accumulate([3, 1, 3, 2]) == [6, 2, 4]

Explain why output order follows the first occurrence rather than sorted order; why a repeated input is removed before doubling; and why expected time is O(n) with O(n) additional memory. Change the requirement to return unique doubled values in sorted order and explain the new sorting cost.

Your debugging diary

Record the failing input, underlying assumption, smallest fix and regression test. Include one performance bug: repeated list membership inside a loop, then an equivalent set-based version. Measure both on growing input sizes without claiming that one timing proves a universal complexity bound.

Checkpoint: explain any two bugs here aloud, recreate the failure and prove your correction. Continue with Testing and DSA practice.

Reference: Python errors and exceptions, Python function defaults.

Course navigation

Course overview · Previous lesson · Next lesson

Section navigation