Preamble
Debugging is a craft, not a personality trait. pdb (or breakpoint() since 3.7) and Java IDE debuggers reward the same habits: reproduce with minimal input, watch data change, and question the assumptions you brought from the last bug. I still learn faster from ten minutes stepping than from an hour of speculative logging—when the state is local and the failure is deterministic.
The sections below are deliberately small programs. Open them in your editor, follow the steps in order, and treat the command list as a cheat sheet you will internalize after a few sessions.
Part 1 — Python: a pdb walkthrough
Sample program
Save as discount.py (the bug is intentional: the second branch never runs when it should).
# discount.py
def apply_discount(price: float, tier: str) -> float:
if tier == "standard":
return price * 0.9
elif tier == "vip":
return price * 0.85
return price
def line_total(quantity: int, unit_price: float, tier: str) -> float:
subtotal = quantity * unit_price
# Bug: tier check uses wrong string — "VIP" vs "vip"
discounted = apply_discount(subtotal, tier.upper())
return round(discounted, 2)
if __name__ == "__main__":
total = line_total(2, 100.0, "vip")
print(total) # expect 170.0, will print 200.0
Step 1 — Start under the debugger
From the same directory:
python -m pdb discount.py
pdb stops before any user code runs. You are at the import/bootstrap phase; type c (continue) once to run until the program finishes, or go to Step 2 to stop earlier.
Step 2 — Drop in with breakpoint()
Edit line_total and add one line after subtotal is computed:
subtotal = quantity * unit_price
breakpoint() # or: import pdb; pdb.set_trace()
discounted = apply_discount(subtotal, tier.upper())
Run normally:
python discount.py
Execution freezes at breakpoint() with a (Pdb) prompt. You are now in the same session you would get from pdb, but only at the line you chose.
Step 3 — Inspect state
At (Pdb):
| Command | What it does |
|---|---|
p subtotal |
Print subtotal (p = print expression). |
p tier |
Shows 'vip'. |
p tier.upper() |
Shows 'VIP' — first hint the callee sees a different string than the dict keys. |
pp locals() |
Pretty-print the current frame’s locals (readable for small dicts). |
Step 4 — Step through the call
| Command | What it does |
|---|---|
s (step) |
Enter apply_discount and stop on its first line. |
n (next) |
Run the current line and stop at the next line in this frame without diving into callees. |
until |
Continue until the next line in this frame above the current line number—handy to skip loops. |
After s into apply_discount, n a few times and watch which branch executes. You should see execution fall through to return price because "VIP" == "vip" is false—evidence without a single print in source.
Step 5 — Finish this frame and return
| Command | What it does |
|---|---|
r (return) |
Run until the current function returns; stop right after return (in the caller). |
c (continue) |
Run until the next breakpoint or program end. |
Use r when you are done reading a helper and want to sit back in the caller.
Step 6 — Navigate the stack
| Command | What it does |
|---|---|
w (where) |
Stack trace: which file, line, and function each frame is in. |
u / d |
Move the “current frame” up (toward caller) or down (toward leaves). After u, p tier might differ from the inner frame’s names—this is how you catch shadowing and wrong arguments. |
l (list) |
Show source around the current line; l . recenters on the instruction pointer. |
Step 7 — Conditional breakpoint without editing (optional)
If you started with python -m pdb discount.py, you can break only when tier is interesting:
(Pdb) b discount.py:13, tier == "vip"
(Pdb) c
When the condition is true, pdb stops on that line. In the breakpoint() workflow, prefer fixing the code to compare case-insensitively or normalizing tier once at the boundary—then re-run and confirm with p at the same stop.
Fix (sanity check)
Normalize tier inside apply_discount or pass tier.lower() from line_total so "VIP" and "vip" agree with your keys. Re-run; c from a breakpoint should reach the end with 170.0.
Part 2 — Java: an IDE debugger walkthrough
Most teams use IntelliJ IDEA, Eclipse, or VS Code with the Java extension. The buttons and shortcuts differ, but the operations are the same: breakpoint, resume, step over, step into, step out, evaluate expression, and conditional breakpoint.
Sample program
Save as src/Discount.java (same logical bug: wrong casing at the call site).
public class Discount {
static double applyDiscount(double price, String tier) {
if ("standard".equals(tier)) {
return price * 0.9;
} else if ("vip".equals(tier)) {
return price * 0.85;
}
return price;
}
static double lineTotal(int quantity, double unitPrice, String tier) {
double subtotal = quantity * unitPrice;
// Bug: VIP vs vip
double discounted = applyDiscount(subtotal, tier.toUpperCase());
return Math.round(discounted * 100.0) / 100.0;
}
public static void main(String[] args) {
double total = lineTotal(2, 100.0, "vip");
System.out.println(total); // expect 170.0, prints 200.0
}
}
Compile and run from the project root:
javac -d out src/Discount.java
java -cp out Discount
Step 1 — Line breakpoint
- Open
Discount.javain the IDE. - Click in the gutter beside the line
double discounted = applyDiscount(...)to set a line breakpoint (red dot).
Debug instead of Run (in IntelliJ: bug icon or Shift+F9; in VS Code: Run and Debug with a Java launch config). The JVM stops before that line executes.
Step 2 — Variables and watches
When suspended:
- The Variables (or Locals) view shows
quantity,unitPrice,tier,subtotal. Confirmtieris"vip"andsubtotalis200.0. - Add a watch for
tier.toUpperCase()— you should see"VIP", which does not match the"vip"branch insideapplyDiscount.
This mirrors the pdb session where tier and tier.upper() disagreed with the dictionary keys.
Step 3 — Step over, into, out
| Action | Typical shortcut (IntelliJ) | Effect |
|---|---|---|
| Step over | F8 | Run the current line; stay in the same method unless the line calls something you are not stepping into. |
| Step into | F7 | Enter applyDiscount and stop on its first line. |
| Step out | Shift+F8 | Run until the current method returns; stop in the caller. |
Step over the applyDiscount call if you only care about the result; step into when you need to see which branch ran—same trade-off as pdb’s n vs s.
Step 4 — Evaluate expression
Open Evaluate Expression (IntelliJ: Alt+F8). Type applyDiscount(200.0, "vip") and evaluate—you should get 170.0. Then try applyDiscount(200.0, "VIP") and see 200.0. You have validated the fix direction without recompiling between guesses.
Step 5 — Conditional breakpoint
Right-click the breakpoint dot → More / Breakpoint properties. Enable Condition and enter something like:
"vip".equals(tier)
Resume (F9). The debugger stops only when tier is "vip", which matches the failing scenario. Use this when a loop or handler runs hundreds of times and only one iteration is wrong.
Step 6 — (Optional) jdb from the terminal
If you want the pdb-style prompt for Java:
javac -g -d out src/Discount.java
jdb -classpath out Discount
At (jdb):
stop in Discount.lineTotal
run
print tier
step
jdb is sparse compared to an IDE, but the mental model—stop, print, step—is the same.
Fix (sanity check)
Pass tier.toLowerCase() into applyDiscount, or compare case-insensitively inside applyDiscount. Debug again; step over the call and confirm discounted is 170.0.
Breakpoints that earn their keep
Conditional breakpoints stop only when user_id == bad_id or when a collection’s size crosses a threshold. In pdb, b file:line, condition; in Java IDEs, the breakpoint’s condition field. Evaluate expression panes let me test a fix hypothesis without recompiling or restarting a giant JVM twice per guess.
Stepping discipline
Step into every frame when you are new to a library; step over when you know the leaf function is boring. Indiscriminate stepping is how thirty-minute bugs become three-hour tours. I combine stepping with targeted logging at system boundaries—HTTP handlers, queue consumers—where correlation IDs already exist (OpenTelemetry Traces Across Python and Java).
Mutable collections and aliasing
Watch lists and maps that mutate through aliases. Python and Java both punish “it changed but I did not assign” confusion. The debugger’s variables view is the honest narrator.
Conclusion
Debugger fluency transfers across languages in this blog’s arc. Refactoring Guided by Tests and Code Smells applies the same test-backed rigor to structural edits; Selected GoF Patterns in Modern Python names GoF patterns where they reduce ambiguity in Python.