AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Debugging Strategies: Finding and Fixing Errors in Python

📚 Computer Science⏱️ 24 min read🎓 Grade 8
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

A Program That Runs Perfectly — and Gives the Wrong Answer

Your class teacher asks you to write a Python program that finds the average of five subject marks: 88, 92, 79, 95, and 84. You type it up, run it, and Python happily prints an answer with no red error text anywhere. Success — except the answer is wrong.

marks = [88, 92, 79, 95, 84]
total = 0
for i in range(len(marks) - 1):
    total = total + marks[i]
average = total / len(marks)
print("Average:", average)

This prints Average: 70.8. But if you add the five numbers by hand — 88 + 92 + 79 + 95 + 84 — you get 438, and 438 divided by 5 is 87.6, not 70.8. The program did not crash. It did not show a single line of red text. It ran from top to bottom, printed a neat, confident-looking number, and that number is simply wrong. This is the single most important fact about debugging that this chapter will teach you: a program that runs without crashing is not the same as a program that is correct. Finding and fixing this kind of silent, confident wrongness is what debugging is really about, and it is a skill you can learn systematically — not a mysterious talent some programmers are born with.

We will come back to this exact program and find its bug properly in a few minutes. First, you need a map of the different ways Python code can go wrong, because each type calls for a different hunting strategy.

Three Kinds of Bugs, Three Kinds of Clues

Every error a beginner Python programmer runs into falls into one of three categories, and confusing them wastes a lot of debugging time.

Syntax errors happen when your code does not follow Python's grammar rules at all — like a sentence in English missing a full stop in a place where the reader absolutely needs one. Python cannot even start running the program; it refuses at the door.

marks = 78
if marks >= 90
    print("A1 grade")
else:
    print("Below A1")

The if line is missing a colon at the end. In a recent Python version (3.10 or later), running this file gives:

  File "grade_check.py", line 2
    if marks >= 90
                  ^
SyntaxError: expected ':'

Notice the little caret (^) — it points to exactly where Python's parser got confused. Older Python versions (before 3.10) were far less helpful here and would just say SyntaxError: invalid syntax, forcing you to scan the line yourself. Either way, the fix is the same: add the missing colon. Syntax errors are, in a strange way, the easiest bugs — Python refuses to run at all, so you cannot ship broken code by accident. You always know immediately that something is wrong.

Runtime errors (also called exceptions) happen when the code is grammatically valid Python, starts running fine, and then hits an instruction it cannot actually carry out with the data it has at that moment. The program starts, does some work, and then stops abruptly partway through, printing a traceback.

Logical errors are the dangerous ones you just saw in the marks-average program. The code is valid Python, it runs completely from start to finish, it never crashes — and it still produces a wrong result, because the logic you wrote does not actually do what you intended it to do. There is no error message at all. The only clue is that the output is wrong, and you only notice that if you already know what the correct answer should be.

Reading a Traceback Like a Detective, Not a Victim

Many students see a traceback, feel a jolt of panic, and stop reading after the first line. That first line is usually the least useful part. Here is a very common beginner mistake, involving reading marks entered by a user:

marks1 = input("Enter Maths marks: ")
marks2 = input("Enter Science marks: ")
marks3 = input("Enter English marks: ")

total = marks1 + marks2 + marks3
average = total / 3
print("Average marks:", average)

Suppose the student types 85, 90, and 78 when prompted. Running this gives:

Traceback (most recent call last):
  File "marks.py", line 6, in <module>
    average = total / 3
TypeError: unsupported operand type(s) for /: 'str' and 'int'

The rule for reading any traceback is: start from the bottom line, then work upward only as far as you need to. The bottom line, TypeError: unsupported operand type(s) for /: 'str' and 'int', tells you the exact category of mistake — Python tried to divide something that was a text string by something that was a whole number, and division between those two types is not defined. The line just above it, average = total / 3, tells you exactly which instruction triggered the crash.

Now here is the trap: the line the traceback points to is where the error was detected, not necessarily where the mistake was actually made. Nothing is wrong with the line average = total / 3 by itself — dividing by 3 is a perfectly normal thing to do. The real mistake happened three lines earlier: input() in Python always returns a string, even if the user types digits. So total = marks1 + marks2 + marks3 did not add three numbers at all — it glued three text strings together end to end, producing the text "859078". That line ran without any error, because + between strings is a valid operation (string concatenation); it just is not the operation the programmer meant. The crash only appeared two lines later, when Python was finally asked to do something (division) that has no meaning for text. The fix belongs at the input lines, not at the division line: marks1 = int(input("Enter Maths marks: ")), and similarly for the other two. This gap between "where Python complained" and "where the bug actually lives" is one of the most important habits of mind in debugging — always ask "what earlier decision made this line fail?", not just "what does this line say?"

A short field guide to exceptions you will meet constantly while learning Python is worth keeping in your head:

  • SyntaxError — the code breaks Python's grammar; nothing runs at all.
  • NameError — you used a variable or function name Python has never seen defined, often a misspelling like toal instead of total.
  • TypeError — you tried an operation on a value of the wrong kind, as in the marks example above.
  • IndexError — you asked a list for a position that does not exist, such as marks[5] when the list only has indices 0 to 4.
  • ZeroDivisionError — you divided a number by zero, which is mathematically undefined and Python refuses to guess an answer.
  • ValueError — the type of the value is right, but its content is unusable for the job, such as int("eighty five"), where the text simply cannot be turned into a whole number.

Knowing these names is not about memorising a glossary. It is about turning a scary wall of red text into a precise, searchable clue the moment you see it.

The Misconception: "It Ran, So It Must Be Right"

Let's name this misconception directly, because it is the single most common reason students hand in wrong homework with total confidence: a program finishing without an error message proves nothing about correctness. Python's interpreter only checks that each instruction is something it knows how to execute. It has absolutely no idea what answer you were hoping for. If you tell it to add the wrong set of numbers, or loop one time too few, it will do exactly that — precisely, quickly, and without a single complaint — because from Python's point of view, you asked it to do that, and it did. The responsibility for checking "is this the right calculation?" sits entirely with you, the programmer, and it never goes away no matter how experienced you get. This is exactly why the marks-average program from the start of this chapter is more dangerous than any traceback: a crash forces you to stop and look; a logical error lets you walk away happily with a wrong report card average.

Strategy 1: Print Statements as X-Ray Vision

Now let's actually catch the bug in the averaging program. The technique is simple: insert print() statements inside the suspicious code so you can see the value of every important variable at every step, instead of only seeing the final answer.

marks = [88, 92, 79, 95, 84]
total = 0
for i in range(len(marks) - 1):
    print("i =", i, " marks[i] =", marks[i], " running total =", total)
    total = total + marks[i]
average = total / len(marks)
print("Average:", average)

Running this prints four lines before the final result:

i = 0  marks[i] = 88  running total = 0
i = 1  marks[i] = 92  running total = 88
i = 2  marks[i] = 79  running total = 180
i = 3  marks[i] = 95  running total = 259
Average: 70.8

Look closely at what is missing: marks has five entries, at index positions 0, 1, 2, 3, and 4, but the printed trace stops at i = 3. Index 4 — the value 84 — was never visited at all. Once you see that gap laid out plainly in front of you, the bug becomes obvious: range(len(marks) - 1) is range(4), and range(4) only produces 0, 1, 2, 3 — four values, one short of the five you needed. This is called an off-by-one error, and it is probably the single most common logical bug in all of programming, in every language, at every skill level. The fix is to remove the stray - 1:

marks = [88, 92, 79, 95, 84]
total = 0
for i in range(len(marks)):
    total = total + marks[i]
average = total / len(marks)
print("Average:", average)

This now correctly prints Average: 87.6. The lesson is not "remember this one bug" — it is the general strategy: when output is wrong but nothing crashes, print the state of your variables at each step of the suspicious loop or calculation, and compare that printed trace against what you expect by hand. The gap between expectation and reality is where the bug hides.

It helps to write this comparison out as a formal dry run before you even touch the keyboard, the way CBSE practical exams often expect you to trace code on paper. For the corrected loop, the trace looks like this:

  1. Before the loop: total = 0
  2. i = 0: marks[0] = 88, so total becomes 0 + 88 = 88
  3. i = 1: marks[1] = 92, so total becomes 88 + 92 = 180
  4. i = 2: marks[2] = 79, so total becomes 180 + 79 = 259
  5. i = 3: marks[3] = 95, so total becomes 259 + 95 = 354
  6. i = 4: marks[4] = 84, so total becomes 354 + 84 = 438
  7. Loop ends (range exhausted). average = 438 / 5 = 87.6

Practising this kind of line-by-line dry run on paper, before you ever run the code, is exactly the skill CBSE Computer Science practical vivas test when they ask you to "trace the output" of a given snippet — and it is the same skill that catches bugs before they even reach the computer.

Strategy 2: Bisection — Debugging Like a Guessing Game

You have almost certainly played a number-guessing game where a friend picks a number from 1 to 100 and you have to find it using only "higher" or "lower" hints. The smart strategy is never to guess 2, then 3, then 4 one at a time — you guess 50 first. If the answer is "lower," you have just eliminated half the numbers in a single question. Guess 25 next, eliminate half of what remains, and within about seven guesses you have narrowed 100 possibilities down to exactly one. This idea — repeatedly cutting the space of possibilities in half — is called binary search, and it turns out to be one of the most powerful debugging strategies too, not just a searching algorithm.

Imagine a much longer program — say, sixty lines that read a list of student records, compute attendance percentages, apply grade boundaries, and print a formatted report — and somewhere in those sixty lines a bug is producing a wrong grade for one student. Reading all sixty lines top to bottom, hoping the bug jumps out at you, is slow and unreliable. The bisection strategy instead says: temporarily disable (comment out, or replace with a fixed dummy value) roughly the second half of the program, and check whether the problem still shows up using just the first half's output. If the wrong value already appears using only the first thirty lines, the bug lives somewhere in those thirty lines, and you can ignore the other thirty completely — you have just eliminated half your search space in one step, exactly like guessing 50 first. If the first half looks correct, the bug must be in the second half instead. Either way, repeat the same halving trick inside whichever half remains guilty: thirty lines becomes fifteen, fifteen becomes seven or eight, and within about six rounds of halving, sixty lines has been narrowed down to a single suspicious line — the same speed advantage that took the number-guessing game from 100 possibilities down to one in about seven guesses. The formal name computer scientists use for this general technique — deliberately disabling or isolating half of a system to figure out which half contains a fault — is bisection debugging, and version control tools like Git even have a built-in command, git bisect, that automates exactly this halving process across a project's history to find which past change introduced a bug.

Strategy 3: Explaining the Code Out Loud (Rubber Duck Debugging)

The third strategy sounds almost too simple to work, and yet professional programmers rely on it every day: explain your code out loud, line by line, to someone else — or even to an inanimate object sitting on your desk, which is why it is nicknamed "rubber duck debugging." The value is not that the duck answers back. The value is that spoken explanation forces you to be fully explicit about every assumption your code is quietly making, in a way that silently reading the same code over and over never does. When you say out loud, "this line takes the average, which divides the total by the number of students," you are forced to actually check: is total really the sum of everyone's marks at this point, or did I only add most of them? Silently re-reading code, your eyes tend to see what you meant to write rather than what you actually wrote — the brain autocorrects, the same way it is easy to miss a typo in your own essay but instantly spot it in a classmate's. Forcing a slow, spoken, step-by-step account of what each line does breaks that autopilot and very often surfaces the bug before you even finish the explanation.

The Debugging Loop, Put Together

These strategies are not alternatives you pick one of — they fit together into a repeatable cycle that professional developers run through, often several times a minute, without consciously naming the steps.

THE DEBUG LOOP repeat until fixed 1. Reproduce the Bug make it fail on demand 2. Read the Traceback bottom line first 3. Isolate (Bisect) halve the suspect code 4. Form a Hypothesis guess the exact cause 5. Test with print() check guess vs reality 6. Fix & Verify re-run the failing case

Notice step 6 loops back into step 1: after applying a fix, you re-run the exact case that originally failed, to verify it now gives the right answer — and you also re-run a few cases that worked before, to make sure your fix did not quietly break something else. A fix that has not been verified against the original failing input is not a fix yet; it is only a guess.

A Working Debugging Checklist

When your code misbehaves, work through these questions in order rather than randomly editing lines and hoping:

  • Does Python refuse to run the file at all? That is a syntax error — find the exact line the caret points to, and check that line and the one just above it for a missing colon, bracket, or quote.
  • Does the program start but crash partway through with a traceback? Read the exception name on the last line first, then find the line number, then ask whether the true cause is that line or an earlier one that set up bad data for it.
  • Does the program finish cleanly but print a wrong answer? This is the hardest and most common case for beginners. Do a hand calculation of what the answer should be, insert print() statements inside every loop and before every calculation, and compare the printed trace against your hand calculation step by step until they diverge.
  • Is the program long and you have no idea which section holds the bug? Bisect it — disable or isolate roughly half the code, check whether the symptom survives, and repeat inside whichever half is still guilty.
  • Still stuck after all that? Explain the suspicious section out loud, one line at a time, as if a classmate had never seen it before.

Practice: Find and Fix the Bug

Each snippet below has exactly one bug. Identify whether it is a syntax error, a runtime error, or a logical error, then state the fix. Try to answer before reading the solutions further down.

# Question 1
price = 250
quantity = 4
total_cost = price * quantity
print("Total: Rs." total_cost)
# Question 2 — intended to print the last item in the list
fruits = ["mango", "guava", "banana"]
print(fruits[3])
# Question 3 — intended to print all even numbers from 2 to 10
n = 2
while n < 10:
    print(n)
    n = n + 2
print(10)

For Question 3, check carefully: does the printed sequence actually match "all even numbers from 2 to 10," or does it just happen to end on the right number for a different reason? Trace it by hand, one loop pass at a time, before deciding it is correct.

Summary

Debugging is not a matter of luck or talent — it is a deliberate process built from three habits. First, correctly classify the failure: a syntax error stops Python before it even starts, a runtime error stops it partway through and hands you a traceback, and a logical error is silent, finishing cleanly while quietly producing a wrong answer, which is why "it ran without crashing" must never be mistaken for "it is correct." Second, use the traceback properly by reading the exception type and message from the bottom line first, then tracing the fault back to its true origin, which is sometimes several lines earlier than the crash itself. Third, when there is no error message at all, manually surface the hidden state of your program using print statements and a hand-worked dry run, narrow a long program down using the same halving idea that makes binary search efficient, and use spoken explanation to break the autopilot that lets your eyes see what you meant to write instead of what you actually wrote. Put together, these form a loop — reproduce, read, isolate, hypothesize, test, fix and verify — that you repeat, sometimes several times within a single bug, until the program's output matches what you can prove by hand is correct.

Answers to the Practice Questions

  • Question 1 — Syntax error. Two values are placed next to each other inside print() with no comma or operator between them ("Total: Rs." total_cost), which Python's grammar does not allow. Fix: add a comma, print("Total: Rs.", total_cost).
  • Question 2 — Runtime error (IndexError). fruits has three items, at valid index positions 0, 1, and 2 — there is no index 3. Fix: the last item is at index len(fruits) - 1, so use print(fruits[2]) or, more generally, print(fruits[-1]).
  • Question 3 — Logical error, but a subtle one. The loop prints 2, 4, 6, 8 (the loop condition n < 10 becomes false once n reaches 10, so the body never runs for n = 10), and then the separate line print(10) outside the loop prints 10 anyway. The final printed sequence — 2, 4, 6, 8, 10 — looks correct by coincidence, but the program does not actually generate it as "even numbers from 2 to 10" through one consistent piece of logic; it patches the missing last value on with a hardcoded line. If someone later changes the range to "2 to 20," the loop will still stop at 18 and the hardcoded print(10) will print a wrong, leftover value. The honest fix is while n <= 10:, which makes the loop itself correctly include 10 and removes the need for the extra patched-on line.
← Tech FutureVersion Control with Git: Collaboration for Young Developers →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn