SECTION 1
Historical Context & Motivation
Every programmer, from beginners to seasoned professionals, encounters bugs — errors that cause a program to behave differently than intended. The word "bug" itself has a fascinating history in computing, and the practice of debugging — the systematic process of finding and fixing these errors — has evolved dramatically over the decades. Understanding where debugging tools came from helps you appreciate the powerful options available to you today in Python.
1947 • The First Literal "Bug"
1960s • Print Statements Emerge
1970s • Interactive Debuggers Appear
2000s • Integrated Debuggers in IDEs
Today • Python's Built-In pdb and IDE Debuggers
The central question that debugging tools address is deceptively simple: What is my program actually doing, and how does that differ from what I expected? Without the ability to peek inside a running program and inspect its state — the current values of all its variables at any given moment — you would be guessing blindly about the source of errors.
SECTION 2
Core Principles of Debugging
Before reaching for any tool, effective debugging begins with a mindset. You need to approach bugs systematically rather than randomly changing code and hoping for the best. The following foundational ideas will guide you every time you encounter unexpected behavior in your programs.
Reproduce the Bug
Understand Program State
Narrow the Search
Form and Test Hypotheses
Choose the Right Tool
KEY TAKEAWAY
SECTION 3
Visualizing the Debugging Process
The diagram below illustrates the two primary approaches to inspecting program state. On the left, print debugging shows how inserting print() calls reveals variable values in the console output. On the right, a step-through debugger pauses execution at a breakpoint and displays the full state in a variables panel.
Notice the key difference: with print debugging, you must decide before running your code exactly which variables to display and where. With a debugger, you set a breakpoint and can explore any variable's value once execution pauses. Both methods let you answer the fundamental question: "What is the state of my program right now?"
SECTION 4
How Print Debugging Works
The print() Function as a Diagnostic Tool
Python's built-in print() function sends text to the console (also called standard output). When you insert print() calls at strategic locations in your code, you create a visible log of what's happening inside the program. The key is choosing what to print and where to place the statement.
Common Print Debugging Patterns
- Variable inspection —
print("x =", x)displays a variable's current value with a label so you know which variable you're looking at. - Checkpoint markers —
print("Reached point A")confirms that execution reached a specific location. Useful when you suspect code is being skipped. - Loop tracing —
print(f"Loop iteration {i}: total = {total}")shows how variables change across each iteration of a loop. - Type checking —
print(type(x))reveals a variable's type, which is critical when operations behave differently for strings versus integers. - F-string formatting — Use f-strings like
print(f"DEBUG: {var_name=}")in Python 3.8+ for a shorthand that prints both the expression and its value.
The F-String Debug Shortcut
Python 3.8 introduced a powerful shortcut for debugging with f-strings. By adding an equals sign after a variable name inside the curly braces, Python automatically prints both the variable name and its value. For example, print(f"{score=}") outputs score=85 rather than just the number. This saves you from typing out the variable name as a separate string label and reduces the chance of labeling errors when debugging multiple variables at once.
Pro Tip: Clean Up After Yourself
print("DEBUG:", so they're easy to search for and remove later.SECTION 5
Understanding the Step-Through Debugger
A step-through debugger is a tool built into most code editors (like VS Code, PyCharm, or Thonny) that lets you pause your program's execution at any line and examine every variable's value at that exact moment. Instead of adding and removing print statements, you set breakpoints — markers that tell the debugger "stop here." Once paused, you can step forward one line at a time and watch the state change in real time.
The diagram shows the six-step workflow you'll follow when using a debugger. The most important concept is the variables panel — it replaces the need for print statements by automatically showing you every variable that exists in the current scope (the region of code where a variable is accessible). The call stack shows the chain of function calls that led to the current line — helpful when functions call other functions.
SECTION 6
Worked Example: Debugging a Faulty Average Calculator
Let's walk through a realistic debugging scenario. Suppose you write a function to calculate the average of a list of test scores, but it produces the wrong answer. We'll use both print debugging and debugger concepts to find and fix the bug.
The Buggy Code
Here is the code with a bug hidden inside:
def average(scores):
total = 0
for i in range(len(scores)):
total += i # Bug is here!
return total / len(scores)
result = average([90, 85, 92, 78])
print(result) # Expected: 86.25, Got: 1.5
Finding the Bug with Print Debugging
- 1
Step 1 — Identify the Symptom
The function returns 1.5 instead of the expected 86.25. This tells us the total being calculated is way too small. The expected total of all scores is 90 + 85 + 92 + 78 = 345, so345 / 4 = 86.25. But we're getting 1.5, which meanstotal = 6(since 6 / 4 = 1.5). Something is very wrong with how total is being calculated.Symptom identified: total is 6 instead of 345 - 2
Step 2 — Add Print Statements Inside the Loop
Insert a print statement to see what's being added on each iteration:print(f"i={i}, scores[i]={scores[i]}, total={total}"). Place this right after the linetotal += iso you can see both the index and the running total.Print statement placed inside the loop body - 3
Step 3 — Read the Output
Running the modified code produces:i=0, scores[0]=90, total=0theni=1, scores[1]=85, total=1theni=2, scores[2]=92, total=3theni=3, scores[3]=78, total=6. Now the problem is obvious — the code is addingi(the index: 0, 1, 2, 3) to total instead ofscores[i](the actual score values).Bug found: total += i should be total += scores[i] - 4
Step 4 — Fix and Verify
Change line 4 fromtotal += itototal += scores[i]. Run the program again. The output is now 86.25 — correct! Finally, remove the debug print statement so the function's output is clean.Fixed! average([90, 85, 92, 78]) → 86.25 ✓
Debugger Alternative
total += i, run in debug mode, and immediately see in the variables panel that i = 0 while scores[0] = 90. You'd notice the code adds the index, not the score. No extra code needed — just set a breakpoint and read the state.SECTION 7
Print Debugging vs. Debugger: Strengths & Limitations
Both print debugging and step-through debuggers are valuable, and experienced programmers use both regularly. The choice depends on the complexity of the bug, your environment, and your comfort level. The table below summarizes the trade-offs.
| Feature | Print Debugging | Step-Through Debugger |
|---|---|---|
| Setup | Zero setup — just add print() calls | Requires IDE with debugger support |
| Speed for simple bugs | Very fast — one line of code | Slightly slower to set up breakpoints |
| Complex bugs | Can require many print statements | Excellent — inspect any variable at any time |
| Code modification | Must edit code; remember to remove later | No code changes needed |
| Learning curve | Minimal — you already know print() | Must learn breakpoints, stepping, panels |
| Exploring state | Only see variables you explicitly print | See all variables in scope automatically |
| Best used when | Quick checks, simple scripts, no IDE | Multi-function programs, loop logic errors |
Comparison of print debugging and step-through debuggers
KEY TAKEAWAY
SECTION 8
Connection to Advanced Debugging Techniques
The print debugging and basic debugger skills you're learning now are the foundation for more sophisticated techniques used by professional software engineers. As your programs grow in size and complexity, you'll encounter debugging challenges that require additional tools. Understanding how today's skills connect to tomorrow's tools helps you see the bigger picture.
| What You Learn Now | Advanced Version | When You'll Need It |
|---|---|---|
print() statements | Logging frameworks (Python's logging module) | When programs run on servers without a console visible to you |
| Setting breakpoints manually | Conditional breakpoints — pause only when a condition is true | When a bug only appears on iteration 5,000 of a loop |
| Inspecting variables in the panel | Watch expressions — track computed values, not just raw variables | When you need to monitor len(my_list) > 100 across execution |
| Stepping through your own code | Unit testing — automated scripts that test your functions | When you want bugs caught automatically before users find them |
| Reading the call stack | Profiling — measuring which functions take the most time | When your program is correct but too slow |
How foundational debugging skills evolve into professional tools
The most important takeaway is that the debugging mindset you're developing now — forming hypotheses, inspecting state, and narrowing down where errors occur — is the same mindset used by every professional developer. The tools get fancier, but the core process stays the same. In future courses, you'll learn about unit testing and test-driven development, which shift debugging from reactive (fixing bugs after they appear) to proactive (preventing bugs from being introduced in the first place).
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
x = 10
y = 3
result = x / y
print(int(result))
The programmer expected the output to be 3.33 (the decimal result of 10 ÷ 3). Instead, they see 3 printed. What print statement would you add to find the bug, and what would it reveal?Show answer
print(f"result = {result}, type = {type(result)}") after line 3 would show that result = 3.3333333333333335 and its type is float. This reveals that the division itself is correct — the bug is on the print line, where int(result) converts the float to an integer, truncating the decimal portion. The fix is to either remove int() entirely (printing result directly gives 3.3333...) or use round(result, 2) if the programmer wants exactly two decimal places. The key insight is that the calculation was never wrong; the output step was the problem.Question 3: INTERMEDIATE
def count_vowels(word):
count = 0
vowels = "aeiou"
for letter in word:
if letter in vowels:
count += 1
return count
print(count_vowels("HELLO"))
Describe how you would use print debugging to find the bug. Show exactly which print statement(s) you'd add and explain what output you'd expect to see.Show answer
print(f"letter = '{letter}', in vowels? {letter in vowels}") inside the for loop would produce output like: letter = 'H', in vowels? False | letter = 'E', in vowels? False | letter = 'L', in vowels? False, and so on. This reveals that even 'E' and 'O' fail the vowel check. The bug is a case-sensitivity issue: the input "HELLO" is uppercase, but the vowels string contains only lowercase letters. 'E' is not equal to 'e' in Python. The fix is to normalize the case by using word.lower() in the loop, changing the for loop to: for letter in word.lower(). Alternatively, the vowels string could include uppercase vowels: "aeiouAEIOU".Question 4: APPLIED
read_scores(), assign_grade(score), and generate_report(scores). Describe a strategy using either print debugging or a debugger to systematically narrow down which function contains the bug. Explain your reasoning at each step.Show answer
Question 5: CRITICAL THINKING
Show answer
SUMMARY
Lesson Summary
Summary
Debugging is the systematic process of finding and fixing errors in your code, and it begins with inspecting program state — the values of all variables at a specific point in execution. The two primary tools at your disposal are print debugging, which uses print() calls inserted into your code to display variable values in the console, and the step-through debugger, which pauses execution at breakpoints and lets you examine every variable in a live variables panel while stepping through code one line at a time.
Effective debugging follows a scientific approach: reproduce the bug, form a hypothesis about what's wrong, inspect state to test your hypothesis, and narrow the search until you find the exact line where reality diverges from expectation. Print debugging is ideal for quick checks in simple scripts, while the debugger excels at complex, multi-function programs. Both tools answer the same fundamental question: "What is my program actually doing?" Mastering both prepares you for logging frameworks, unit testing, and other professional practices you'll encounter in advanced courses.