SECTION 1
Historical Context & Motivation
If you have ever added a dozen print() statements throughout your code just to figure out why a variable has the wrong value, you already understand the problem that debuggers were invented to solve. Early programmers literally had to trace through machine instructions by hand, sometimes reading punched cards one at a time. As programs grew larger and more complex, the need for automated tools to pause execution and examine a program's internal state became critical.
The word "bug" in computing dates back to the 1940s, when a moth was found stuck in a relay of the Harvard Mark II computer. Since then, the concept of debugging — the systematic process of finding and removing errors — has evolved from hand-tracing to sophisticated interactive tools. Python's built-in debugger, pdb, and modern IDE debuggers (like those in VS Code or PyCharm) give you the power to freeze your program mid-run and look under the hood.
1947 • The First "Bug"
1970s • Early Interactive Debuggers
1991 • Python & pdb Are Born
2010s • IDE Debuggers Go Mainstream
2018 • Python 3.7 Adds breakpoint()
So the central question is: how can you move beyond scattering print() statements and instead use a structured, interactive approach to find bugs? That is exactly what breakpoints, stepping, and inspecting are designed to do.
SECTION 2
Core Principles of Debugging
Debugging with pdb or an IDE revolves around three fundamental actions. These three concepts form the backbone of every debugging session, whether you are using a simple terminal-based tool or a full graphical IDE. Understanding them turns you from someone who guesses at bugs into someone who systematically hunts them down.
Breakpoint
Step
Inspect
Continue
KEY TAKEAWAY
print() debugging feels like.SECTION 3
Visual Explanation — The Debugging Workflow
The diagram below illustrates the typical debugging workflow. You start by writing code, then set a breakpoint at a suspicious line. When you run the program in debug mode, execution pauses at that line. From there you can inspect variables, step through lines one at a time, or continue to the next breakpoint. This cycle repeats until you find and fix the bug.
Notice that debugging is a cycle, not a straight line. After you step through a few lines and inspect variables, you might continue running to the next breakpoint, find more clues, and step again. The key insight is that at every pause, you have complete access to your program's state — every variable, every data structure, and even the order of function calls that brought you to that point.
SECTION 4
How pdb and IDE Debuggers Work
Python's pdb module is a text-based interactive debugger that comes built into Python's standard library. You do not need to install anything — it is always available. There are two main ways to activate it in your code.
Method 1: The breakpoint() Function (Python 3.7+)
Simply write breakpoint() on any line of your program. When Python reaches that line, it drops you into the pdb interactive prompt, where you can type commands to inspect and step through your code.
Method 2: import pdb; pdb.set_trace()
Before Python 3.7, the standard way was to write import pdb; pdb.set_trace() at the line where you wanted to pause. This does the same thing as breakpoint() but requires the explicit import. You will see both in online tutorials, and they are functionally equivalent.
Essential pdb Commands
| Command | Shortcut | What It Does |
|---|---|---|
next | n | Execute the current line and move to the next one (Step Over). |
step | s | Step Into the current function call to see what happens inside it. |
continue | c | Resume normal execution until the next breakpoint is hit. |
print(var) | p var | Print the current value of a variable (Inspect). |
list | l | Show the source code around the current line. |
quit | q | Exit the debugger and stop the program. |
Core pdb commands you will use most often.
IDE Debugging: The Graphical Approach
If you use an IDE like VS Code or PyCharm, you get the same breakpoint/step/inspect capabilities but with a graphical interface. Instead of typing commands, you click in the margin next to a line number to set a breakpoint (usually shown as a red dot). You then press a "Run and Debug" button, and when execution pauses, a panel shows you all current variables, the call stack (the chain of function calls), and toolbar buttons for Step Over, Step Into, Step Out, and Continue. The concepts are identical to pdb — the interface is just more visual.
SECTION 5
Stepping Modes Explained
One of the trickiest parts of debugging is understanding the difference between the various stepping modes. When your program calls a function, should the debugger jump inside that function, or treat it as a single operation and move on? The diagram below clarifies the three stepping options.
Quick Tip
return (shortcut r). In IDE debuggers like VS Code, it is a button with an upward arrow icon labeled "Step Out." The idea is the same: finish running the current function and go back to wherever it was called from.SECTION 6
Worked Example — Debugging a Buggy Function
Let us walk through a realistic debugging session. Consider this function that is supposed to compute the average of a list of numbers, but it returns the wrong answer.
Here is the buggy code:
def average(numbers):
total = 0
for num in numbers:
total += num
return total / len(numbers) + 1 # Bug is here!
When we call average([10, 20, 30]), we expect 20.0 but get 21.0. Let us debug it step by step using pdb.
Debugging average() with pdb
- 1
Step 1 — Insert a Breakpoint
Addbreakpoint()at the top of the function, right afterdef average(numbers):. This will pause execution as soon as the function is called.Code now reads:def average(numbers): breakpoint() total = 0 ... - 2
Step 2 — Run the Program
Run your script withpython my_script.py. The program starts and immediately pauses at the breakpoint. You see the pdb prompt:(Pdb).The terminal shows:> my_script.py(3)average() -> total = 0 (Pdb) - 3
Step 3 — Inspect the Input
Typep numbersto print the value of the numbers parameter. This confirms the function received the correct input.Output:[10, 20, 30]— looks correct. - 4
Step 4 — Step Through the Loop
Typenrepeatedly to step over each line. After the loop finishes, typep totalto check the accumulated sum.Output:60— that is correct (10 + 20 + 30 = 60). - 5
Step 5 — Inspect the Return Expression
Before the return line executes, typep total / len(numbers) + 1to evaluate the expression. Python's order of operations calculates 60 / 3 first (getting 20.0) and then adds 1, giving 21.0. The+ 1is the bug! It should not be there, or it needs parentheses.Bug found! The fix is to remove+ 1from the return statement. The correct line isreturn total / len(numbers).
Remember
breakpoint() call so your program runs normally in production. Leaving breakpoints in shipped code is a common beginner mistake!SECTION 7
pdb vs IDE Debugging vs print()
Now that you understand both approaches, let us compare them directly. Each method has its place, and experienced developers use all three depending on the situation.
| Feature | print() Debugging | pdb (Terminal) | IDE Debugger |
|---|---|---|---|
| Setup required | None — just add print() | None — built into Python | Need an IDE installed |
| Pause execution? | No — program runs to the end | Yes — pauses at breakpoints | Yes — pauses at breakpoints |
| Inspect variables | Only what you print | Any variable, any expression | Any variable, with a GUI panel |
| Step through code | Not possible | Yes — n, s, r commands | Yes — toolbar buttons |
| Learning curve | Very easy | Moderate — need to learn commands | Moderate — need to learn the UI |
| Best for | Quick checks, simple scripts | Remote servers, any environment | Complex projects with many files |
Comparison of three debugging approaches.
KEY TAKEAWAY
print() debugging like shining a flashlight in a dark room — you can only see where you point it. A proper debugger is like turning on all the lights at once. You see everything: every variable, every function call, every piece of data. Use print() for quick checks, but reach for pdb or your IDE debugger when the bug is tricky or you do not even know where to start looking.SECTION 8
Connection to Advanced Debugging
The breakpoint/step/inspect cycle you learned here is the foundation, but professional debugging tools offer even more power. As you advance, you will encounter these more sophisticated features.
| Introductory Concept | Advanced Version |
|---|---|
| Simple breakpoints (pause at a line) | Conditional breakpoints — pause only when a condition is true, e.g., only when x > 100 |
| Inspect one variable at a time | Watch expressions — automatically track how a value changes over time |
| pdb in the terminal | Remote debugging — connect your IDE to code running on a different machine or server |
| Step through line by line | Post-mortem debugging — examine the program state at the exact moment an unhandled exception occurred |
| Manual breakpoint placement | Logging frameworks — strategic, permanent instrumentation using Python's logging module |
How introductory debugging concepts connect to advanced techniques.
You do not need to learn all of these right now. The important thing is that mastering breakpoints, stepping, and inspecting gives you a transferable skill. Every programming language — JavaScript, Java, C++, Go — has debuggers with these same three concepts. Once you internalize them in Python, you can debug anywhere.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
print() statement when debugging. Why might a breakpoint be more useful than a print statement for finding complex bugs?Show answer
print() statement shows you only the specific value you chose to print, and the program keeps running past it. A breakpoint pauses the entire program at that line, letting you inspect any variable, evaluate expressions, and step through subsequent lines one at a time. For complex bugs, you often don't know which variable to look at in advance. A breakpoint lets you explore freely, which is why it is more powerful for tricky issues.Question 2: BASIC CALCULATION
breakpoint() on line 3:
x = 5
y = x * 2
breakpoint()
z = y + 3
print(z)
When the debugger pauses, what values will p x and p y show? What about p z?Show answer
p x shows 5 and p y shows 10 (since 5 × 2 = 10). However, p z would produce a NameError because line 4 has not executed yet — z has not been defined. The breakpoint pauses before the line it sits on, but after all lines above it.Question 3: INTERMEDIATE
def multiply(a, b):
return a * b
def process(values):
breakpoint()
result = 1
for v in values:
result = multiply(result, v)
return result
print(process([2, 3, 4]))
You want to see what happens inside multiply() during the first loop iteration. What pdb commands would you type, in order, to get there?Show answer
n to execute result = 1, then n to enter the for loop, then s (step into) when the debugger reaches result = multiply(result, v). Using s at the multiply call enters the function, pausing at return a * b. You can then use p a and p b to see the arguments (1 and 2 on the first iteration).Question 4: APPLIED
def count_vowels(text):
count = 0
vowels = 'aeiou'
for char in text:
if char in vowels:
count + 1
return count
Describe how you would use pdb to identify the bug. Which line would you place the breakpoint on, and which variable would you inspect after each iteration?Show answer
breakpoint() inside the for loop, right before the if statement. Step through with n and inspect count after each iteration. You would notice that even when the if condition is True and the line count + 1 executes, the value of count stays at 0. The bug is that count + 1 computes a value but does not assign it back to count. The fix is count += 1 (or count = count + 1). The debugger makes this obvious because you can watch count remain unchanged.Question 5: CRITICAL THINKING
breakpoint() to debug a live web server. What alternatives from the "Advanced Debugging" section (Section 8) would be more appropriate, and why?Show answer
breakpoint() to a live web server would freeze the entire server when it hits the breakpoint, blocking all user requests — this is unacceptable in production. Instead, you would use logging frameworks to record variable values and program flow without pausing execution. Conditional breakpoints could be used in a staging environment to pause only when a specific condition triggers the bug. Remote debugging could connect an IDE debugger to a test copy of the server. Post-mortem debugging could examine the state after an error occurs without affecting live traffic. The key insight is that interactive debugging requires pausing, which is only appropriate during development, not on live systems.SUMMARY
Summary
Summary
Python provides powerful debugging tools that go far beyond print() statements. The built-in pdb module and modern IDE debuggers both revolve around three core actions: setting breakpoints to pause execution at a specific line, stepping through code one line at a time (using Step Over, Step Into, and Step Out), and inspecting variables and expressions while the program is paused.
You can activate pdb using breakpoint() (Python 3.7+) or import pdb; pdb.set_trace(). The essential commands are n (next / step over), s (step into), c (continue), p (print / inspect), and q (quit). These same concepts apply to every IDE debugger and transfer to every programming language, making this one of the most universally useful skills in computer science.