DSA Tutorial
🔍

Dry Run & Debug Approach

Dry Run & Debug Approach

You've written code that looks correct, but it fails on some test cases. Or worse, you can't understand why it works. Sound familiar?

Dry running (manually tracing code) and systematic debugging are the most underrated skills in programming. Master these, and you'll spend 10x less time debugging.

Goal: Learn to trace through code like a computer and debug issues systematically.

Key Insight: If you can't dry run your code with a simple example, you don't truly understand it. Dry running turns mysterious bugs into obvious fixes.

What is Dry Running?

Dry running means manually executing code step-by-step, tracking all variables, just like a computer would.

Why Dry Run?

1. Verify Correctness

  • Catch logic errors before running code
  • Ensure algorithm works for edge cases

2. Understand Code

  • See exactly what each line does
  • Understand why algorithm works

3. Debug Efficiently

  • Pinpoint exact line where logic fails
  • Understand unexpected behavior

4. Interview Success

  • Demonstrate understanding to interviewers
  • Catch mistakes before submitting

The Dry Run Method

Follow these steps for any code:

Step 1: Choose a Simple Test Case

Pick an example that:

  • Is small enough to trace manually (3-5 elements)
  • Covers the main logic
  • Isn't too trivial

Example: For array problems, use [3, 1, 4, 2] instead of [1, 2, 3]

Step 2: Set Up a Trace Table

Create columns for:

  • Line number or step
  • Each variable
  • Important conditions
  • Output/result

Step 3: Execute Line by Line

For each line:

  • Update variable values
  • Check conditions
  • Record changes
  • Note any output

Step 4: Verify Result

Check:

  • Final output matches expected
  • All variables have expected values
  • No unexpected state

Example 1: Find Maximum in Array

Let's dry run this simple algorithm:

1def find_max(arr): 2 max_val = arr[0] # Line 1 3 4 for i in range(1, len(arr)): # Line 2 5 if arr[i] > max_val: # Line 3 6 max_val = arr[i] # Line 4 7 8 return max_val # Line 5 9 10# Test case 11result = find_max([3, 1, 4, 2])

Dry Run Trace Table:

Input: arr = [3, 1, 4, 2]

StepLineiarr[i]max_valCondition (arr[i] > max_val)Action
11--3-Initialize max_val
22113-Start loop
331131 > 3 = FalseSkip update
42243-Next iteration
532434 > 3 = TrueEnter if
64244-Update max_val
72324-Next iteration
833242 > 4 = FalseSkip update
924-4-Loop ends
105--4-Return 4

Result: 4 (Correct!)

Verification: The algorithm correctly found the maximum value.

Example 2: Two Sum Problem

Let's dry run a more complex algorithm:

1def two_sum(nums, target): 2 seen = {} # Line 1 3 4 for i, num in enumerate(nums): # Line 2 5 complement = target - num # Line 3 6 7 if complement in seen: # Line 4 8 return [seen[complement], i] # Line 5 9 10 seen[num] = i # Line 6 11 12 return [] # Line 7 13 14# Test case 15result = two_sum([2, 7, 11, 15], 9)

Dry Run Trace Table:

Input: nums = [2, 7, 11, 15], target = 9

StepLineinumcomplementseenConditionAction
11---{}-Initialize empty map
2202-{}-Start loop
33027{}-Calculate 9-2=7
44027{}7 in {} = FalseSkip return
56027{2:0}-Add 2→0 to map
6217-{2:0}-Next iteration
73172{2:0}-Calculate 9-7=2
84172{2:0}2 in {2:0} = TrueFound!
95172{2:0}-Return [0, 1]

Result: [0, 1] (Correct!)

Verification: nums[0] + nums[1] = 2 + 7 = 9

Example 3: Binary Search

Let's dry run a recursive algorithm:

1def binary_search(arr, target, left, right): 2 if left > right: # Line 1 3 return -1 # Line 2 4 5 mid = (left + right) // 2 # Line 3 6 7 if arr[mid] == target: # Line 4 8 return mid # Line 5 9 elif arr[mid] < target: # Line 6 10 return binary_search(arr, target, mid + 1, right) # Line 7 11 else: 12 return binary_search(arr, target, left, mid - 1) # Line 8 13 14# Test case 15result = binary_search([1, 3, 5, 7, 9], 7, 0, 4)

Dry Run with Recursion Tree:

Input: arr = [1, 3, 5, 7, 9], target = 7

Call 1: binarySearch(arr, 7, 0, 4) ├─ left=0, right=4, mid=2 ├─ arr[2]=5, 5 < 7 └─ Recurse right: binarySearch(arr, 7, 3, 4) | Call 2: binarySearch(arr, 7, 3, 4) ├─ left=3, right=4, mid=3 ├─ arr[3]=7, 7 == 7 └─ Return 3 ✓

Trace Table:

Callleftrightmidarr[mid]ComparisonAction
104255 < 7Search right half
234377 == 7Return 3

Result: 3 (Correct!)

Verification: arr[3] = 7

Systematic Debugging Approach

When code doesn't work, follow this process:

Step 1: Reproduce the Bug

Actions:

  • Identify the failing test case
  • Run the code with that input
  • Confirm the bug is consistent

Example:

Input: [1, 2, 3] Expected: 6 Got: 3 Bug confirmed!

Step 2: Understand Expected vs Actual

Questions:

  • What should happen?
  • What actually happens?
  • Where do they diverge?

Example:

Expected: Sum all elements → 1+2+3=6 Actual: Only returns first element → 3 Hypothesis: Not iterating through all elements

Step 3: Add Strategic Print Statements

Where to add prints:

  • Before and after key operations
  • Inside loops (with iteration number)
  • At function entry/exit
  • Variable changes
1def buggy_sum(arr): 2 total = 0 3 print(f"Starting sum, arr={arr}") # Entry point 4 5 for i in range(len(arr)): 6 print(f" Iteration {i}: total={total}, arr[i]={arr[i]}") # Loop state 7 total += arr[i] 8 print(f" After add: total={total}") # After operation 9 10 print(f"Final total: {total}") # Exit point 11 return total

Step 4: Dry Run the Failing Case

Trace manually:

  • Use the exact input that fails
  • Follow every line
  • Compare with expected behavior

Step 5: Form Hypothesis

Based on dry run:

  • Identify where logic diverges
  • Guess the root cause
  • Predict the fix

Example:

Hypothesis: Loop starts at index 1 instead of 0 Evidence: First element is skipped Fix: Change range(1, len(arr)) to range(len(arr))

Step 6: Test the Fix

After fixing:

  • Run original failing test
  • Run edge cases
  • Run all test cases
  • Verify no new bugs introduced

Common Bug Patterns

Pattern 1: Off-by-One Errors

Symptoms:

  • Skipping first/last element
  • Array index out of bounds
  • Wrong final result

Example Bug:

1# Bug: Skips last element 2def sum_array_bug(arr): 3 total = 0 4 for i in range(len(arr) - 1): # ❌ Should be len(arr) 5 total += arr[i] 6 return total 7 8# Fix 9def sum_array_fix(arr): 10 total = 0 11 for i in range(len(arr)): # ✓ Correct 12 total += arr[i] 13 return total 14 15# Dry run with [1, 2, 3]: 16# Bug: i goes 0,1 → skips arr[2] → returns 3 17# Fix: i goes 0,1,2 → includes all → returns 6

Pattern 2: Wrong Variable Update

Symptoms:

  • Variable doesn't change as expected
  • Infinite loops
  • Wrong final state

Example Bug:

1# Bug: Updates wrong variable 2def find_max_bug(arr): 3 max_val = arr[0] 4 current = arr[0] # Extra variable 5 6 for num in arr: 7 if num > max_val: 8 current = num # ❌ Updates wrong variable 9 10 return max_val # Returns unchanged value 11 12# Fix 13def find_max_fix(arr): 14 max_val = arr[0] 15 16 for num in arr: 17 if num > max_val: 18 max_val = num # ✓ Updates correct variable 19 20 return max_val 21 22# Dry run with [3, 5, 1]: 23# Bug: max_val stays 3, current becomes 5 24# Fix: max_val becomes 5

Pattern 3: Wrong Condition

Symptoms:

  • Logic executes at wrong time
  • Missing cases
  • Incorrect branching

Example Bug:

1# Bug: Uses > instead of >= 2def binary_search_bug(arr, target): 3 left, right = 0, len(arr) - 1 4 5 while left > right: # ❌ Should be >= 6 mid = (left + right) // 2 7 if arr[mid] == target: 8 return mid 9 elif arr[mid] < target: 10 left = mid + 1 11 else: 12 right = mid - 1 13 return -1 14 15# Fix 16def binary_search_fix(arr, target): 17 left, right = 0, len(arr) - 1 18 19 while left <= right: # ✓ Correct 20 mid = (left + right) // 2 21 if arr[mid] == target: 22 return mid 23 elif arr[mid] < target: 24 left = mid + 1 25 else: 26 right = mid - 1 27 return -1 28 29# Dry run with [1, 3, 5], target=1: 30# Bug: left=0, right=2, left>right is False, loop never runs 31# Fix: left=0, right=2, left<=right is True, finds element

Debugging Checklist

When debugging, check:

Logic Issues:

  • Are loop bounds correct? (off-by-one)
  • Are conditions correct? (>, >=, <, <=, ==, !=)
  • Are all variables initialized?
  • Are variables updated correctly?
  • Are edge cases handled?

Data Issues:

  • Are indices within bounds?
  • Are null/empty cases handled?
  • Are data types correct?
  • Are comparisons using correct operators?

Flow Issues:

  • Does control flow match expectations?
  • Are return statements in right places?
  • Are break/continue used correctly?
  • Are recursive base cases correct?

Tips for Effective Dry Running

Tip 1: Use Small Inputs

Bad: Dry run with array of 100 elements
Good: Dry run with [3, 1, 4] - small but non-trivial

Tip 2: Write It Down

Bad: Trace in your head
Good: Use paper or table - prevents mistakes

Tip 3: Be Methodical

Bad: Jump around code
Good: Execute line by line, no shortcuts

Tip 4: Check Edge Cases

Dry run with:

  • Empty input
  • Single element
  • All same values
  • Already sorted
  • Reverse sorted

Tip 5: Trace Both Paths

For conditionals:

  • Dry run when condition is true
  • Dry run when condition is false

Interview Dry Run Strategy

When Asked to Dry Run:

Step 1: "Let me trace through with a simple example"

Step 2: Write the example clearly

Step 3: Create a simple table (or use space efficiently)

Step 4: Talk through each step aloud

Step 5: Highlight the result

Example:

Interviewer: Walk me through how this works You: Let me trace through with [3, 1, 4]. Starting with max_val = 3. At i=1, arr[1]=1, 1 is not > 3, so skip. At i=2, arr[2]=4, 4 > 3, so update max_val to 4. Return 4, which is correct.

Key Takeaways

Dry Running:

  • Execute code manually, step by step
  • Use a trace table to track variables
  • Verify logic with simple test cases
  • Catch bugs before running code

Debugging:

  • Reproduce the bug consistently
  • Add strategic print statements
  • Dry run the failing case
  • Form hypothesis, test fix
  • Check all test cases after fixing

Remember: If you can dry run it, you understand it. If you can't, you don't.

What's Next?

Now that you can dry run and debug:

  1. Testing Strategies - Write comprehensive tests
  2. Common Patterns - Practice dry running patterns
  3. Practice Problems - Apply dry running to real problems

Practice Exercise

Dry run this code with input [2, 7, 11, 15], target = 18:

1def mystery(arr, target): 2 for i in range(len(arr)): 3 for j in range(i + 1, len(arr)): 4 if arr[i] + arr[j] == target: 5 return [i, j] 6 return []

Create your trace table and find the answer!

Answer: [1, 2] (because arr[1] + arr[2] = 7 + 11 = 18)

Congratulations! You now have systematic dry running and debugging skills!