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

Debugging Tools — I can use print debugging or a debugger conceptually to inspect state at my level.

Learn to track down and fix bugs by inspecting what your program is actually doing at each step.

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"
Grace Hopper's team at Harvard found a moth trapped in the Mark II computer's relay, coining the term "debugging." Early programmers had to inspect hardware directly to find problems.
1960s
Print Statements Emerge
As programming languages like FORTRAN and COBOL grew in popularity, programmers began inserting output statements into their code to display variable values during execution — the birth of print debugging.
1970s
Interactive Debuggers Appear
Tools like GDB (GNU Debugger) allowed programmers to pause execution, step through code line by line, and inspect memory — a major leap beyond print statements.
2000s
Integrated Debuggers in IDEs
Modern development environments like Visual Studio, Eclipse, and later VS Code bundled graphical debuggers directly into the editor, making breakpoints and variable inspection accessible to everyone.
Today
Python's Built-In pdb and IDE Debuggers
Python ships with the pdb module, and popular editors like VS Code, PyCharm, and Thonny offer visual debugging with a single click. Print debugging remains widely used alongside these tools.

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.

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.

1

Reproduce the Bug

Before you can fix a bug, you must be able to trigger it reliably. Run your program with the same inputs that caused the error so you can observe the problem consistently.
2

Understand Program State

State means the values of all variables at a specific point in execution. Bugs happen when state doesn't match your expectations. Inspecting state is the core skill of debugging.
3

Narrow the Search

Use a strategy like binary search: check the state halfway through your code. If it's correct there, the bug is in the second half. This eliminates large chunks of code quickly.
4

Form and Test Hypotheses

Think like a scientist. Form a hypothesis about what's wrong, design a small test (a print statement or breakpoint), check whether the evidence supports or contradicts your guess, and adjust.
5

Choose the Right Tool

Simple bugs may need only a quick print statement. Complex bugs with many interacting variables benefit from a step-through debugger. Matching the tool to the problem saves time.
✦ KEY TAKEAWAY
Think of debugging like being a detective at a crime scene. The "crime" is the bug, and your evidence is the program's state. Print statements are like asking witnesses what they saw — you get specific answers to specific questions. A debugger is like security camera footage — you can pause, rewind, and study every detail of what happened, frame by frame.

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.

Left: print debugging adds explicit output calls to reveal values in the console. Right: a step-through debugger pauses at a breakpoint (red dot) and shows all variable values in a live panel, with controls to step through execution one line at a time.

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?"

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
Always remove or comment out your debugging print statements once you've found and fixed the bug. Leftover debug output clutters your program's real output and can confuse users. A good practice is to prefix debug prints with a distinctive tag like print("DEBUG:", so they're easy to search for and remove later.

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 debugger workflow: set a breakpoint, run in debug mode, inspect variables, and step through line by line until you identify where the state diverges from your expectations. Key terms are shown in the cards below the flow.

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.

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 SymptomThe 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, so 345 / 4 = 86.25. But we're getting 1.5, which means total = 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 LoopInsert 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 line total += i so you can see both the index and the running total.
Print statement placed inside the loop body
3
Step 3 — Read the OutputRunning the modified code produces: i=0, scores[0]=90, total=0 then i=1, scores[1]=85, total=1 then i=2, scores[2]=92, total=3 then i=3, scores[3]=78, total=6. Now the problem is obvious — the code is adding i (the index: 0, 1, 2, 3) to total instead of scores[i] (the actual score values).
Bug found: total += i should be total += scores[i]
4
Step 4 — Fix and VerifyChange line 4 from total += i to total += 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
With a debugger, you would set a breakpoint on the line 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.

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.

Comparison of print debugging and step-through debuggers
FeaturePrint DebuggingStep-Through Debugger
SetupZero setup — just add print() callsRequires IDE with debugger support
Speed for simple bugsVery fast — one line of codeSlightly slower to set up breakpoints
Complex bugsCan require many print statementsExcellent — inspect any variable at any time
Code modificationMust edit code; remember to remove laterNo code changes needed
Learning curveMinimal — you already know print()Must learn breakpoints, stepping, panels
Exploring stateOnly see variables you explicitly printSee all variables in scope automatically
Best used whenQuick checks, simple scripts, no IDEMulti-function programs, loop logic errors
✦ KEY TAKEAWAY
Don't think of these as competing tools — think of them as different-sized wrenches in a toolbox. You wouldn't use a pipe wrench to tighten a small bolt, and you wouldn't use pliers on a large pipe fitting. Print debugging is your quick-access wrench for small tasks. The debugger is your adjustable wrench for complex jobs. Professional developers keep both ready and switch between them as needed.

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.

How foundational debugging skills evolve into professional tools
What You Learn NowAdvanced VersionWhen You'll Need It
print() statementsLogging frameworks (Python's logging module)When programs run on servers without a console visible to you
Setting breakpoints manuallyConditional breakpoints — pause only when a condition is trueWhen a bug only appears on iteration 5,000 of a loop
Inspecting variables in the panelWatch expressions — track computed values, not just raw variablesWhen you need to monitor len(my_list) > 100 across execution
Stepping through your own codeUnit testing — automated scripts that test your functionsWhen you want bugs caught automatically before users find them
Reading the call stackProfiling — measuring which functions take the most timeWhen your program is correct but too slow

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).

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words what program state means. Why is inspecting state the central activity of debugging?
PROBLEM 2 — BASIC CALCULATION
Consider this code: 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?
PROBLEM 3 — INTERMEDIATE
A student writes this function to count vowels in a word, but it always returns 0: 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.
PROBLEM 4 — APPLIED
You're building a simple grade calculator for a class. The program reads a list of scores and assigns letter grades, but some students are receiving incorrect grades. The program is too long to share here, but it has three functions: 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.
PROBLEM 5 — CRITICAL THINKING
A classmate argues: "Print debugging is a waste of time. You should always use the debugger because it shows you everything." Another classmate counters: "I never use the debugger — print statements are faster and easier." Evaluate both positions. In what specific scenarios would each approach be more effective? Can you think of a situation where neither tool alone would be sufficient?

Lesson 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.

Varsity Tutors • Computer Programming 1 (Python) • Debugging Tools — I can use print debugging or a debugger conceptually to inspect state at my level.