COMPUTER PROGRAMMING 1 (PYTHON) • DEBUGGING, TESTING & CODE QUALITY

Systematic Debugging — I can debug systematically (reproduce, isolate, fix, retest) and explain what changed.

Learn the four-step cycle that transforms frustrating bugs into learning opportunities and cleaner code.

Historical Context & Motivation

Every programmer, from beginners to experts, encounters bugs. The word "bug" itself has a fascinating origin in computing history. Before systematic methods existed, developers would hunt for errors by reading through every single line of code, sometimes for hours or even days. As programs grew larger and more complex, this brute-force approach became impossible, and the need for a structured debugging process became urgent.

1947
The First Literal Bug
Grace Hopper's team found a moth stuck in a relay of the Harvard Mark II computer. They taped it into their logbook and labeled it the "first actual case of bug being found." The term debugging entered the programming vocabulary.
1969
Debugging in the Space Race
During the Apollo 11 moon landing, software alarms fired mid-descent. Engineers had to quickly reproduce, isolate, and resolve the issue under extreme pressure, demonstrating why systematic methods matter in critical software.
1991
Python is Born
Guido van Rossum released Python, which included readable error messages and a built-in debugger (pdb). Python's design philosophy prioritized making bugs easier to find and fix.
2000s
Modern IDE Debugging Tools
Integrated Development Environments like VS Code and PyCharm introduced visual debuggers with breakpoints, variable watchers, and step-through execution, making the four-step debugging cycle accessible to everyone.

The core question this lesson addresses is: How can you move from randomly changing code and hoping for the best to a reliable, repeatable process that consistently finds and fixes errors? The answer is systematic debugging — a four-step cycle of reproduce, isolate, fix, and retest.

Core Principles of Systematic Debugging

Systematic debugging is not about being a genius who spots errors instantly. It is a disciplined process that anyone can learn and apply. The four steps form a cycle because fixing one bug sometimes reveals another, and the process repeats until the program works correctly. Understanding these core principles will give you a framework you can rely on every time something goes wrong in your code.

1

Reproduce

Run the code with specific inputs that trigger the bug every time. If you cannot make the bug happen on demand, you cannot verify that you have fixed it later.
2

Isolate

Narrow down exactly which line or block of code is causing the problem. Use print statements, comments, or a debugger to test sections independently.
3

Fix

Change only the code that is broken. Make one change at a time so you know exactly what solved the problem. Avoid the temptation to rewrite large sections.
4

Retest

Run the same inputs from the reproduce step to confirm the bug is gone. Also test other inputs to make sure your fix did not break anything else.
✦ KEY TAKEAWAY
Think of debugging like a doctor diagnosing a patient. You would not want your doctor to guess randomly — instead, they ask about your symptoms (reproduce), run targeted tests to find the source (isolate), prescribe a specific treatment (fix), and schedule a follow-up to confirm you are healthy (retest). Systematic debugging follows the same logical process for sick code.

The Debugging Cycle Visualized

The four-step debugging cycle: Reproduce → Isolate → Fix → Retest. If the bug persists after retesting, the cycle repeats from reproduction with new information.

Notice how the diagram forms a loop, not a straight line. This is intentional. In real-world debugging, your first fix attempt might not work, or it might reveal a deeper problem. The key insight is that each pass through the cycle gives you more information about what is really happening in your code. Even a failed fix teaches you something — it tells you what the bug is not, narrowing down your search.

How Each Step Works in Python

Step 1: Reproduce — Triggering the Bug on Demand

Reproducing a bug means finding the exact inputs, steps, and conditions that cause the error. Write down the specific inputs you used, the expected output, and the actual output. For example, if your function calculate_average([10, 20, 30]) returns 6.0 instead of 20.0, record all three pieces of information. This becomes your test case for verifying the fix later.

Step 2: Isolate — Narrowing Down the Suspect

Isolation is the most skill-intensive step. You have several techniques at your disposal. Print debugging uses print() statements to display variable values at different points in your code. Binary search debugging means commenting out half your code to see if the bug still occurs, then narrowing further. Reading the traceback in Python tells you the exact file, line number, and error type. A traceback reads from bottom to top — the last line shows the error, and lines above it show the chain of function calls that led there.

Step 3: Fix — Changing Only What Is Broken

The golden rule of fixing is: change one thing at a time. If you change five things simultaneously and the bug disappears, you will not know which change actually fixed it. Worse, one of those other changes might introduce a new bug that shows up later. Make a single, targeted change, then immediately move to retesting.

Step 4: Retest — Confirming the Fix

Retesting means running your original failing test case and additional test cases. Your original input should now produce the correct output. But you also need to check edge cases — what happens with an empty list, a list with one element, or negative numbers? A fix that breaks other functionality is not really a fix. This step catches regression bugs — new problems created by your changes.

💡 Pro Tip: The Debugging Log
Keep a short written log while debugging. Write down what you tried, what you observed, and what you concluded. This prevents you from going in circles and trying the same thing twice. Even a few sticky notes can save you hours of frustration.

Common Bug Types in Python

Not all bugs behave the same way. Understanding the category of bug you are dealing with helps you choose the right isolation technique. Python bugs generally fall into three major categories: syntax errors, runtime errors, and logic errors. Each requires a different debugging approach.

Comparison of the three bug types. Syntax errors are the easiest to fix because Python tells you exactly where they are. Logic errors are the hardest because the program runs without crashing — you only discover them by checking your output against expected values.

Syntax errors are usually the quickest to fix because Python points directly to the problem line. Runtime errors require you to trace the flow of your program to understand what state the variables were in when the crash happened. Logic errors are the trickiest because your code runs perfectly fine — it just produces the wrong result. For logic errors, the isolate step becomes critical: you need to add print statements to check variable values at each stage and find where the actual values diverge from what you expected.

Worked Example: Debugging a Grade Calculator

Let us walk through a complete debugging session. A student wrote a function to calculate a letter grade from a percentage, but it is returning wrong results. Here is the buggy code:

🐛 Buggy Code
def get_grade(score): if score >= 90: return "A" if score >= 80: return "B" if score >= 70: return "C" if score > 60: return "D" else: return "F" When the student calls get_grade(60), it returns "F" instead of "D".
Systematic Debugging Session
1
Step 1 — ReproduceWe run print(get_grade(60)) and confirm it outputs "F". We also test other values: get_grade(95) returns "A" (correct), get_grade(85) returns "B" (correct). The bug specifically affects a score of exactly 60.
Bug reproduced: get_grade(60) → "F" (expected "D")
2
Step 2 — IsolateWe look at the condition for the D grade: if score > 60. When score is exactly 60, the condition 60 > 60 evaluates to False because 60 is not strictly greater than 60. The code falls through to the else clause and returns "F". The bug is a wrong comparison operator — it should be >= instead of >.
Root cause found: > should be >= on the D grade condition
3
Step 3 — FixWe change if score > 60 to if score >= 60. This is one small, targeted change. We do not modify any other part of the function.
Single change applied: score > 60 → score >= 60
4
Step 4 — RetestWe rerun get_grade(60) and it now returns "D". We also test edge cases: get_grade(59) returns "F" (correct), get_grade(61) returns "D" (correct), get_grade(90) returns "A" (still correct). No regressions detected.
✓ Bug fixed, all test cases pass
5
Step 5 — Explain What ChangedThe original code used a strict greater-than operator (>) for the D grade threshold, while all other grade thresholds used greater-than-or-equal-to (>=). This inconsistency caused scores of exactly 60 to skip the D range and fall into the F range. The fix was changing > to >= so that a score of 60 is correctly included as a D.
Explanation complete: boundary condition error fixed by using >= instead of >

Debugging Techniques: Strengths & Limitations

There are several techniques you can use during the isolate step. Each has its strengths and weaknesses, and experienced programmers switch between them depending on the situation. The table below compares the three most common approaches for beginning Python programmers.

Common debugging techniques for Python beginners
TechniqueHow It WorksStrengthsLimitations
Print StatementsAdd print() calls to display variable values at key pointsSimple, no extra tools needed, works everywhereClutters code, must remove after, can produce huge output
Commenting Out CodeTemporarily disable sections with # to narrow the location of the bugQuick binary search through code, no output clutterCan break dependencies between sections, may hide the bug
Reading the TracebackRead Python's error message from bottom to top to find the crash locationExact line number and error type, fastest for runtime errorsOnly works for runtime errors, not logic errors (no crash = no traceback)
✦ KEY TAKEAWAY
Think of debugging techniques like tools in a toolbox. A hammer is great for nails but useless for screws. Similarly, reading the traceback is your best first move for runtime errors, but for logic errors where the program does not crash, you need print statements or commenting-out to investigate. Knowing when to use which tool is just as important as knowing the tools exist.

From Print Debugging to Professional Tools

The four-step debugging cycle you have learned in this lesson is the same process used by professional software engineers worldwide. As you advance, the tools become more powerful, but the underlying logic never changes. Here is how each step evolves as you grow as a programmer.

How debugging techniques evolve from beginner to professional
StepBeginner Approach (This Course)Advanced Approach (Future Courses)
ReproduceManually run the program with specific inputsWrite automated test cases using frameworks like pytest
IsolateUse print() statements and commentingUse IDE debuggers with breakpoints and variable watchers
FixEdit the code directly and saveUse version control (Git) to track and revert changes
RetestManually re-run test inputsRun an automated test suite that checks all cases at once

In future courses, you will also encounter concepts like unit testing, where you write small tests for individual functions before you even write the function itself. You will learn about breakpoints, which let you pause your program mid-execution and inspect every variable. And you will use version control to save snapshots of your code so you can always undo a fix that made things worse. All of these advanced tools are built on the same four-step foundation you are learning today.

Practice Problems

PROBLEM 1 — CONCEPTUAL
A classmate's program crashes, and they immediately start changing random lines of code to see if that helps. Which step of the systematic debugging cycle are they skipping, and why is that step important?
PROBLEM 2 — BASIC CALCULATION
Consider this buggy function: def double_all(numbers): for i in range(len(numbers)): numbers[i] = numbers[i] * 3 return numbers The function is supposed to double every number in the list. When called with double_all([2, 5, 10]), it returns [6, 15, 30] instead of [4, 10, 20]. Identify the bug and write the corrected line.
PROBLEM 3 — INTERMEDIATE
The following function should return the largest number in a list, but it has a bug: def find_max(numbers): biggest = 0 for num in numbers: if num > biggest: biggest = num return biggest It works correctly for find_max([3, 7, 2]) (returns 7), but fails for find_max([-5, -1, -8]) (returns 0 instead of −1). Walk through all four debugging steps and explain your fix.
PROBLEM 4 — APPLIED
You are building a simple quiz app. The function below checks if a user's answer matches the correct answer, but users are complaining that correct answers are being marked wrong: def check_answer(user_input, correct): if user_input == correct: return "Correct!" else: return "Wrong!" When a user types "Paris" and the correct answer is "paris", the function returns "Wrong!". Debug this systematically and propose a fix that handles both capitalization differences and accidental leading/trailing spaces.
PROBLEM 5 — CRITICAL THINKING
A student debugs a function and says: "I fixed the bug by rewriting the entire function from scratch." Evaluate this approach. Does it follow the systematic debugging cycle? What are the risks of this strategy, and when might it actually be justified? Support your reasoning with references to specific steps in the cycle.

Lesson Summary

Systematic debugging is a four-step cycle that transforms frustrating errors into manageable problems. First, you reproduce the bug by identifying the exact inputs and conditions that trigger it. Second, you isolate the root cause using techniques like print statements, commenting out code, or reading Python tracebacks. Third, you fix the bug by making a single, targeted change — never rewriting large sections unnecessarily. Fourth, you retest with your original failing input and additional edge cases to confirm the fix works without introducing regressions.

Python bugs fall into three categories: syntax errors (code cannot run), runtime errors (code crashes mid-execution), and logic errors (code runs but gives wrong results). The final and equally important part of debugging is being able to explain what changed — articulating the root cause and how your fix addressed it. This skill proves you understand the bug, not just that you stumbled into a solution.

Varsity Tutors • Computer Programming 1 (Python) • Systematic Debugging