Reading code the way Python runs it — tracing tables, error messages, test cases, edge cases, and the habits that turn confusion into understanding
Every programmer, from beginner to expert, writes programs that do not work correctly on the first attempt. This is not a sign of failure. It is a normal part of programming. What separates programmers who grow from programmers who get stuck is not whether they make mistakes. It is whether they have the skills and habits to find and fix those mistakes.
This chapter is about exactly those skills. You have now written programs with variables, expressions, input and output, conditionals, and loops. Your programs have grown more complex. That means there are more places for something to go wrong — and more reasons to develop a disciplined approach to reading, tracing, and testing your programs.
A program that runs without crashing is not the same thing as a program that is correct. Python will happily execute a flawed plan from beginning to end and print a confident, wrong answer. The only defense against this is a set of habits: tracing code carefully, reading error messages closely, testing with inputs you already know the answer to, and building programs in small, verified pieces. This chapter teaches those habits before your programs grow large enough that mistakes become hard to find.
By the end of this chapter, you should be able to:
ZeroDivisionError, ValueError, and NameError.When you read a sentence, your eyes move forward and your brain follows the meaning. When you trace code, you follow the computer — one instruction at a time — and track what changes.
Tracing is the practice of reading your program the way Python executes it: in order, keeping track of variable values, following the path through conditions and loops, and noting what gets printed or returned.
Beginners often skip tracing. They write code, run it, and then feel confused when the output is wrong. The confusion comes from not knowing what the program actually did. Tracing eliminates that confusion.
score = 50 bonus = 10 score = score + bonus print(score)
If you trace this program, you follow it line by line:
score is assigned the value 50.bonus is assigned the value 10.score + bonus, which is 50 + 10 = 60. The result is assigned back to score. Now score is 60.60.60
That was easy because the program is small. But the same approach applies when programs are larger. You read each line in the order Python runs it, you update your mental picture of every variable, and you follow every branch and every loop iteration.
Tracing is not about memorizing what Python does. It is about thinking carefully about what each line means. Every skilled programmer traces code — sometimes in their head, sometimes on paper, sometimes by adding extra print statements to watch variables as a program runs.
A tracing table is a simple tool for keeping track of variable values as a program runs. You create a column for each variable, a column for any output, and a row for each step in the program. Then you fill it in as you read through the code.
count = 0 total = 0 count = count + 1 total = total + 5 count = count + 1 total = total + 8 print(count) print(total)
The variables are count and total. The tracing table looks like this:
| Step | Line | count | total | Output |
|---|---|---|---|---|
| 1 | count = 0 | 0 | — | |
| 2 | total = 0 | 0 | 0 | |
| 3 | count = count + 1 | 1 | 0 | |
| 4 | total = total + 5 | 1 | 5 | |
| 5 | count = count + 1 | 2 | 5 | |
| 6 | total = total + 8 | 2 | 13 | |
| 7 | print(count) | 2 | 13 | 2 |
| 8 | print(total) | 2 | 13 | 13 |
The program prints 2 and then 13.
Now trace a program with a loop:
total = 0
for number in range(1, 5):
total = total + number
print(total)
range(1, 5) produces the sequence 1, 2, 3, 4.
| Iteration | number | total |
|---|---|---|
| Start | — | 0 |
| 1st | 1 | 1 |
| 2nd | 2 | 3 |
| 3rd | 3 | 6 |
| 4th | 4 | 10 |
After the loop ends, print(total) outputs 10.
When you trace carefully, you often discover the error before you even run the program. Students who skip tracing tend to change code randomly and feel lucky when it works. Programmers who trace code understand why it works — or why it doesn't.
Python produces an error message whenever it cannot execute your program as written. The error message is not punishment. It is information. Learning to read error messages clearly is one of the most important debugging skills you can develop.
Here is a typical Python error message:
Traceback (most recent call last):
File "quiz.py", line 7, in <module>
score = score + points
NameError: name 'points' is not defined
Every Python error message has the same structure:
quiz.py) and the line number (line 7) where the error occurred. This is where you should start looking.score = score + points.NameError) and a short message (name 'points' is not defined). This tells you exactly what kind of problem Python encountered.Reading the bottom line first is often the fastest way to understand what went wrong. Once you know the error type, look at the line number, read that line in your program, and ask yourself why that error would occur there.
A syntax error means that Python cannot even understand what you wrote. The code does not follow the rules of the Python language, so Python refuses to run the program at all.
Common causes of syntax errors:
if, for, while, or defscore = 85
if score >= 60
print("Passing")
Running this produces:
File "grades.py", line 3
if score >= 60
^
SyntaxError: expected ':'
Python points directly at the problem: the colon is missing at the end of the if statement.
if score >= 60:
print("Passing")
Syntax errors are actually the easiest errors to fix because Python tells you almost exactly where to look.
A runtime error is an error that Python does not detect until the program is actually running. The syntax was valid enough for Python to start, but something went wrong during execution.
Common runtime errors include:
"hello" to an integeritems = 0 total = 100 average = total / items print(average)
Traceback (most recent call last):
File "average.py", line 3, in <module>
average = total / items
ZeroDivisionError: division by zero
Python ran the first two lines fine, but failed at line 3 because dividing by zero is impossible.
age = int("twenty")
print(age)
Traceback (most recent call last):
File "age.py", line 1, in <module>
age = int("twenty")
ValueError: invalid literal for int() with base 10: 'twenty'
The string "twenty" cannot be converted to an integer. Python expected digits, not letters.
print(result)
Traceback (most recent call last):
File "result.py", line 1, in <module>
print(result)
NameError: name 'result' is not defined
Python has never seen a variable named result. You are trying to use a variable that does not exist yet.
A logic error is the most difficult kind of error to find. The program runs without crashing, but it produces the wrong result. Python cannot detect logic errors because, from Python's perspective, the program is valid and complete. The mistake is in your thinking, not in the syntax or the types.
# Calculate the average of three scores score1 = 80 score2 = 90 score3 = 70 average = score1 + score2 + score3 / 3 print(average)
Expected output: 80.0
Actual output: 193.33333333333334
The program ran perfectly. No errors. But the answer is wrong.
The problem is order of operations. Division has higher precedence than addition, so Python evaluated score3 / 3 first, producing 23.333..., and then added score1 + score2 + 23.333... to get 193.333....
average = (score1 + score2 + score3) / 3 print(average)
Now the output is 80.0.
Another classic logic error involves = versus ==:
password = "open123"
if password = "open123": # This is actually a SyntaxError in Python
print("Welcome!")
Python actually catches this specific mistake as a syntax error, but the confusion between assignment (=) and comparison (==) causes many logic errors in conditions that are harder to spot.
# Count how many scores are passing (60 or above)
scores = [55, 70, 45, 90, 60]
passing = 0
for score in scores:
if score > 60: # Bug: should be >= 60
passing = passing + 1
print(passing)
With the > instead of >=, a score of exactly 60 is not counted as passing. The program runs without error, but the result is wrong for any input containing a score of exactly 60.
Logic errors are dangerous precisely because nothing alerts you to them. A program with a syntax error or runtime error announces itself. A program with a logic error confidently hands you a wrong answer that looks correct unless you check it.
Print-style debugging is the practice of adding temporary print() statements to your program so you can watch variables change as the program runs. It is one of the oldest and most reliable debugging techniques in programming.
When a program produces a wrong result and you cannot see why, add print statements to show you what the program is actually doing.
numbers = [1, 2, 3, 4, 5, 6]
total = 0
for number in numbers:
if number % 2 == 0:
total = total + number
print(total)
Suppose you are not sure whether the condition is working correctly. Add a print statement inside the loop:
numbers = [1, 2, 3, 4, 5, 6]
total = 0
for number in numbers:
print("Checking:", number, " total so far:", total) # Debug print
if number % 2 == 0:
total = total + number
print("Final total:", total)
Checking: 1 total so far: 0 Checking: 2 total so far: 0 Checking: 3 total so far: 2 Checking: 4 total so far: 2 Checking: 5 total so far: 6 Checking: 6 total so far: 6 Final total: 12
Now you can see exactly what happens at every step. The program is working correctly: it adds 2, 4, and 6, and the final total is 12.
Add a label to every debug print so you know which print statement produced which line.
Print the variable name alongside its value: print("total:", total) is more useful than print(total).
Remove or comment out debug print statements when you are done. Leaving them in makes programs messy.
Add debug prints at the point where you suspect something goes wrong, not randomly throughout the program.
Print-style debugging is powerful because it shows you the actual values your program uses, not the values you assumed it would use.
A test case is a specific input, together with the output you expect for that input. Writing test cases before you are done coding is one of the most important habits in programming.
def is_passing(score):
if score >= 60:
return True
else:
return False
Test cases for this function:
| Input (score) | Expected Output | Reason |
|---|---|---|
| 90 | True | Well above passing |
| 60 | True | Exactly at the boundary |
| 59 | False | One point below passing |
| 0 | False | Lowest possible score |
| 100 | True | Perfect score |
To test each case manually:
print(is_passing(90)) # Expected: True print(is_passing(60)) # Expected: True print(is_passing(59)) # Expected: False print(is_passing(0)) # Expected: False print(is_passing(100)) # Expected: True
If any of these prints the wrong value, you have found a bug.
Without test cases, you can only say "I think my program is correct." With test cases, you can say "My program produces the right output for these specific inputs." Test cases give you evidence, not just hope.
Good test cases cover:
An edge case is an input that is at the boundary, extreme, or unusual end of what a program might receive. Edge cases are where programs most often fail, because programmers tend to test typical cases and forget the unusual ones.
Consider this program that finds the largest number in a list:
def find_largest(numbers):
largest = 0
for number in numbers:
if number > largest:
largest = number
return largest
Test with typical input:
print(find_largest([3, 7, 2, 9, 1])) # Expected: 9
Output: 9. Looks correct.
Now test with an edge case — all negative numbers:
print(find_largest([-3, -7, -2, -9, -1])) # Expected: -1
Output: 0. Wrong.
largest = 0 assumes the largest value is at least zero. But if all numbers are negative, the function never finds anything larger than 0 and incorrectly returns 0.
def find_largest(numbers):
largest = numbers[0] # Start with the first item, not zero
for number in numbers:
if number > largest:
largest = number
return largest
Now the function works correctly even when all numbers are negative.
The habit of asking "what if the input is unusual?" and then testing that unusual input catches many bugs that normal testing misses.
Incremental development means building a program in small pieces and testing each piece before moving to the next. Instead of writing the entire program and then running it for the first time, you write a small portion, test it, confirm it works, and then extend it.
Incremental development makes debugging far easier. When you write 50 lines and run the program for the first time, an error could be anywhere in those 50 lines. When you write 5 lines, test them, confirm they work, and then write 5 more, you always know that any new error is almost certainly in the new code you just added.
Here is an example. Suppose you want to build a program that reads a list of quiz scores from the user, then prints the total, the average, and the highest score.
scores = []
count = int(input("How many scores? "))
for i in range(count):
score = float(input("Enter score: "))
scores.append(score)
print("Scores entered:", scores)
scores = []
count = int(input("How many scores? "))
for i in range(count):
score = float(input("Enter score: "))
scores.append(score)
total = sum(scores)
print("Total:", total)
scores = []
count = int(input("How many scores? "))
for i in range(count):
score = float(input("Enter score: "))
scores.append(score)
total = sum(scores)
average = total / count
print("Total:", total)
print("Average:", average)
scores = []
count = int(input("How many scores? "))
for i in range(count):
score = float(input("Enter score: "))
scores.append(score)
total = sum(scores)
average = total / count
highest = max(scores)
print("Total:", total)
print("Average:", average)
print("Highest:", highest)
Each step added one new idea. Each step was tested. If something broke, it was clear exactly where the new problem was introduced.
Get one part working before building the next.
Test early and test often.
Never add new code to code that is already broken.
If something stops working, think about what you just changed.
Read each program below carefully. Answer the questions without running the program. Then verify your answers by running the program.
x = 10 y = 3 z = x // y print(z) print(x % y)
z?x % y evaluate to?count = 5
while count > 0:
print(count)
count = count - 2
count take?result = 0
for i in range(1, 6):
if i % 2 != 0:
result = result + i
print(result)
i satisfy the condition i % 2 != 0?result equal after the loop?word = "programming"
count = 0
for letter in word:
if letter in "aeiou":
count = count + 1
print(count)
count?Complete a tracing table for each program below.
a = 1
b = 10
while a < b:
a = a * 2
b = b - 1
print(a)
print(b)
Build a tracing table with columns for a, b, and the loop condition. Trace each iteration. What does the program print?
total = 0
count = 0
numbers = [4, 7, 2, 9, 3]
for number in numbers:
if number > 5:
total = total + number
count = count + 1
print(total)
print(count)
Trace the values of total and count after each iteration. What does the program print?
password = ""
attempts = 0
while password != "open" and attempts < 3:
password = input("Enter password: ")
attempts = attempts + 1
if password == "open":
print("Access granted.")
else:
print("Access denied.")
Trace this program if the user enters "wrong", then "wrong", then "open". What does the program print? Now trace it if the user enters "wrong" three times. What does the program print?
For each program, predict exactly what will be printed before running it.
for i in range(3):
for j in range(3):
print(i, j)
x = 100
y = 0
if x > 50:
y = y + 10
if x > 75:
y = y + 10
if x > 90:
y = y + 10
print(y)
values = [10, 20, 30, 40, 50]
result = values[0]
for i in range(1, len(values)):
result = result + values[i]
print(i, result)
Each program below contains an error. Identify the type of error (syntax, runtime, or logic), explain what the error is, and write the corrected code.
total = 0
for i in range(1, 11)
total = total + i
print(total)
What type of error is this? What is missing? Write the corrected program.
items = [] average = sum(items) / len(items) print(average)
What type of error is this? When does it occur? What would you need to check before computing the average? Write a corrected version that handles an empty list.
# Count how many numbers are greater than the average
numbers = [10, 20, 30, 40, 50]
total = 0
for number in numbers:
total = total + number
average = total / len(numbers)
above_count = 0
for number in numbers:
if number > average:
above_count = above_count + 1
print("Numbers above average:", above_count)
This program runs without error. Is the output correct? What should it print for this list? Trace through the second loop carefully.
# A program to reverse a string
word = "python"
reversed_word = ""
for i in range(len(word)):
reversed_word = word[i] + reversed_word
print(reversed_word)
Trace through this loop. What does reversed_word look like after each iteration? Does the program produce the correct result?
# Find the smallest number in a list
def find_smallest(numbers):
smallest = 0
for number in numbers:
if number < smallest:
smallest = number
return smallest
print(find_smallest([5, 3, 8, 1, 4]))
print(find_smallest([-2, -5, -1, -9]))
What does this function return for each call? Is that correct? Identify the logic error and fix it.
This worked example shows how to build a small program incrementally, test it with test cases, and use debugging to find and fix a logic error.
Goal: Write a program that takes a list of five quiz scores, then prints the total, the average, and whether the student is passing (average of 60 or above).
scores = [70, 55, 80, 65, 90]
total = 0
for score in scores:
total = total + score
print("Total:", total)
Test this. Expected total: 70 + 55 + 80 + 65 + 90 = 360. Output: Total: 360. Correct.
scores = [70, 55, 80, 65, 90]
total = 0
for score in scores:
total = total + score
average = total / len(scores)
print("Total:", total)
print("Average:", average)
Expected average: 360 / 5 = 72.0. Output: Average: 72.0. Correct.
scores = [70, 55, 80, 65, 90]
total = 0
for score in scores:
total = total + score
average = total / len(scores)
print("Total:", total)
print("Average:", average)
if average > 60:
print("Result: Passing")
else:
print("Result: Not Passing")
Test with the existing scores. Average is 72.0, which is above 60, so "Passing" is printed. Correct.
Try an average of exactly 60. Change the scores to [60, 60, 60, 60, 60].
Expected output: Passing (60 is a passing average).
The condition is average > 60, not average >= 60. With an average of exactly 60, the program prints "Not Passing." That is a logic error.
if average >= 60:
print("Result: Passing")
else:
print("Result: Not Passing")
Test cases used:
| Scores | Expected Average | Expected Result |
|---|---|---|
| [70, 55, 80, 65, 90] | 72.0 | Passing |
| [60, 60, 60, 60, 60] | 60.0 | Passing |
| [50, 55, 45, 50, 60] | 52.0 | Not Passing |
| [100, 100, 100, 100, 100] | 100.0 | Passing |
| [0, 0, 0, 0, 0] | 0.0 | Not Passing |
The boundary case (average of exactly 60) revealed the logic error. Without testing that case, the program would have appeared correct.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
even_count = 0
for number in numbers:
if number % 2 = 0:
even_count = even_count + 1
print(even_count)
count = 10
while count > 0:
print(count)
count = count - 1
Fix the indentation error and verify your fix by tracing the corrected program.
is_valid_age(age) that returns True if age is an integer between 0 and 120 inclusive, and False otherwise. Write test cases for typical values, boundary values (0 and 120), and out-of-range values (-1 and 121).Below is a quiz program that has multiple errors. Some are syntax errors, some are logic errors, and one will cause a runtime error under certain inputs. Your job is to find all of them, fix them, add proper test cases, and leave the program in a clean, working state.
# Quiz program - 3 questions
score = 0
print("Welcome to the Python Quiz!")
print()
# Question 1
answer = input("What keyword starts a conditional in Python? ")
if answer == "if":
score = score + 1
print("Correct!")
else
print("Incorrect. The answer is: if")
print()
# Question 2
answer = input("What function displays output in Python? ")
if answer = "print":
score = score + 1
print("Correct!")
else:
print("Incorrect. The answer is: print()")
print()
# Question 3
answer = input("What is 7 times 8? ")
if answer == 56:
score = score + 1
print("Correct!")
else:
print("Incorrect. The answer is: 56")
print()
print("Quiz complete!")
print("Your score:", score, "out of 3")
percentage = score / 3 * 100
print("Percentage:", percentage)
if percentage > 70:
print("Good job!")
elif percentage > 50:
print("Keep practicing.")
else:
print("Review the material and try again.")
These questions review concepts from Chapters 5 through 10.
while loop to ask the user for a positive number. Keep asking until the user enters a value greater than zero. Then print the square root of that number. Hint: x ** 0.5 computes the square root of x.m in the alphabet (that is, the letters a through l). Find the logic error.
word = "programming"
for letter in word:
if letter < "m":
print(letter)
Is this program correct? Trace through it. Does Python's comparison of single characters work the way you expect? Test it with a few different words.
TypeError: can only concatenate str (not "int") to str. Explain what this error means. What likely caused it? How would you fix it?n = 1
while n != 10:
n = n + 3
print(n)
Will this loop ever end? Trace through several iterations. What happens?
def find_maximum(numbers):
maximum = 0
for number in numbers:
if number > maximum:
maximum = number
return maximum
The student tests it with [3, 7, 2, 9, 1] and gets 9. They think the function is correct. What test case would reveal that it has a bug? What is the bug?
Chapter 10 closes the control flow unit by giving you the skills to verify and repair programs you write. You now have a systematic way to trace, classify errors, test, and debug. Chapter 11 opens Part III by introducing functions — named, reusable blocks of code that let you organize larger programs. You have been writing short scripts where all the code runs from top to bottom. Functions allow you to give a block of code a name, call it from multiple places, and avoid repeating the same instructions throughout a program. You will learn how to define a function, how to call a function, how execution moves into and back out of a function, and how functions help you organize programs that would otherwise become long and hard to follow. The tracing and debugging skills you built in Chapter 10 will carry directly into Chapter 11, because function calls add a new dimension to tracing: you will track not just variable values, but where in the program execution is happening.