print() debugging works, but it requires guessing in advance exactly what information you'll need, then re-running the program after every guess. An interactive debugger lets you pause execution at an exact point and inspect the ENTIRE live program state, no re-running or advance guessing required.
1The Structural Limitation of print()-Debugging
print() debugging has a specific, structural limitation worth naming precisely: it requires deciding, *in advance*, exactly which values might be relevant to understanding a bug ā before you've actually seen the bug's full behavior, before you know exactly what's wrong. You add a print(item), run the program, discover that's not quite enough information, add print(total) too, run again, realize you also need to know which iteration this is, add another print ā an iterative, re-running-heavy cycle where each guess about what to inspect costs a full re-run to test.
An interactive debugger's core capability directly removes this specific limitation: a breakpoint, set by clicking in VS Code's editor gutter (the space to the left of line numbers) next to a specific line, tells the debugger to *pause execution entirely* ā not print something and continue, but genuinely suspend the running program ā the instant that line is about to execute. Starting the program under the debugger (F5, or the Run and Debug panel) runs normally until it hits that breakpoint, then freezes there, with the entire program's state at that exact moment fully intact and inspectable.
This reframes debugging from 'guess what to print, re-run, repeat' into 'pause at the right moment, then look at literally everything' ā the second approach requires deciding *where* to pause (a much easier decision than deciding exactly *what values* to print), and then lets you explore the full state interactively from there, without needing to have anticipated in advance exactly which specific piece of information would turn out to matter.
def calculate_total(items: list[dict]) -> float:
total = 0.0
for item in items:
total += item["price"] * item["quantity"] # <- click here to set a breakpoint
return total
# Running with F5 (Start Debugging) pauses execution exactly at this line,
# on EVERY iteration -- before that specific line actually executesExecution genuinely pauses ā full state available, no advance guessing
2The Variables Panel: Everything, Live, No Code Required
Once execution is paused at a breakpoint, VS Code's Variables panel displays the complete current scope's state automatically ā every local variable, its current value, and (for complex objects like dicts, lists, or custom class instances) an expandable tree letting you drill into nested structure ā all without a single line of debugging-specific code having been written anywhere in the program itself. item, total, and items are all simply *there*, live, exactly as they exist in the running program at that precise paused moment.
This is a genuinely different category of information access than print() debugging can provide: you're not looking at a snapshot someone deliberately chose to capture and format as a string at some point in the past ā you're looking at the actual, live, currently-in-memory state of the running program, which you can explore interactively (expanding a nested dict, checking a different variable you hadn't originally thought to print) *after* pausing, based on what you discover once you're actually looking at the real situation, rather than needing to have anticipated every useful piece of information before ever running the code.
Beyond the Variables panel, VS Code's debugger also provides a Debug Console ā an interactive prompt, available while paused, where you can type and evaluate arbitrary Python expressions against the current live scope (item["price"] * 1.1, testing a hypothesis about what the correct value *should* be, directly against the actual paused state) ā extending the 'explore live state interactively' capability beyond just viewing pre-existing variables into genuinely testing new expressions on the spot.
# At the breakpoint, the Variables panel shows (live, no code needed):
# item = {"price": 29.99, "quantity": 3}
# total = 59.98
# items = [...] (the full list, expandable)
#
# Compare to print()-debugging: you'd need to have GUESSED to print
# exactly these values, in advance, before even seeing the bugThe full live scope, explorable interactively ā not a pre-chosen snapshot
3Conditional Breakpoints: Pausing Only When It Actually Matters
A plain breakpoint inside a loop pauses on *every single iteration* ā for a bug that specifically manifests on, say, the 500th iteration of a loop processing a large list, an unconditional breakpoint set on the loop body would require manually clicking 'continue' 499 separate times just to reach the iteration that actually exhibits the problem, an genuinely tedious and impractical workflow for anything beyond a small handful of iterations.
A conditional breakpoint ā set by right-clicking an existing breakpoint and choosing 'Edit Breakpoint' to add an expression, like item["quantity"] < 0 ā changes this fundamentally: execution now only actually pauses when that specific boolean expression evaluates to true, running through every iteration where the condition is false at full, unpaused speed, and stopping precisely, only, at the moment the condition you actually care about becomes true. For the 500th-iteration bug, a condition checking whatever specifically distinguishes that problematic iteration (a specific value, an index number, a particular data shape) takes you directly there.
This capability is precisely what makes interactive debugging practical for bugs that only manifest under specific, hard-to-predict conditions deep inside a loop, a recursive call, or any code path executed many times ā rather than debugging being limited to bugs that happen to occur on the very first execution of a piece of code, conditional breakpoints extend the same 'pause and inspect everything' capability to bugs that only reveal themselves after a large amount of normal, uninteresting execution has already happened.
# Right-click a breakpoint -> Edit Breakpoint -> Expression:
# item["quantity"] < 0
#
# Now execution only PAUSES when that specific condition is true --
# not on every one of potentially thousands of loop iterationsPauses only at the iteration that actually matters ā not all 499 before it
4Step-by-Step Breakdown
print()-debugging a tricky bug means guessing what to print, running, seeing it's not enough, adding another print, running again. A debugger lets you pause once and look at everything.
A breakpoint (click in the gutter, left of the line number) pauses execution AT that exact line -- before it runs -- letting you inspect everything at that precise moment.
Once paused, the VARIABLES panel shows the ENTIRE current scope's state -- every local variable, live, inspectable, without a single print() statement.
Checkpoint: What is the key advantage of the Variables panel over print()-statement debugging?
- āIt shows the ENTIRE current scope live, without needing to have guessed in advance exactly which values would be worth printing
- āIt also prints values to the console, just formatted more nicely
A CONDITIONAL breakpoint only pauses when a specific expression is true -- essential for a bug that only appears on the 500th iteration of a loop, not the first.
Checkpoint: Why is a conditional breakpoint essential for a bug that only appears on a loop's 500th iteration?
- āAn unconditional breakpoint would pause on EVERY iteration, requiring 499 manual "continue" clicks before reaching the one that actually matters
- āIt makes the loop itself execute faster
That completes Python Developer Tools ā Ruff, Black, mypy, pre-commit, and now the interactive debugger give you the complete professional tooling foundation. Next, Python with AI covers using AI assistance effectively within this same professional workflow.
Evaluate a Real Conditional Breakpoint. Finish should_pause(): a conditional breakpoint only pauses when its expression is True.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Reach for a breakpoint instead of print() for any bug requiring more than one or two quick print statements to diagnose
Print debugging's iterative guess-run-guess-again cycle costs real time for anything beyond the simplest cases -- a breakpoint's full, live state inspection is faster and more thorough for genuinely non-trivial debugging.
Use conditional breakpoints for bugs inside loops or recursive calls that only manifest under specific conditions
An unconditional breakpoint inside a loop requires manually continuing past every uninteresting iteration -- a conditional breakpoint goes directly to the iteration that actually exhibits the problem.
Frequent Bugs
Debugging a bug that only manifests deep inside a large loop or many recursive calls using print() statements or an unconditional breakpoint, requiring either scrolling through enormous console output or manually continuing past hundreds of uninteresting pauses.
Use a conditional breakpoint with an expression identifying the specific condition under which the bug actually occurs, pausing execution only at the moment that matters.
Real-World Examples
Debugging an Intermittent Data-Processing Bug With a Conditional Breakpoint
A data pipeline processing thousands of records fails intermittently, and the team needs to find the exact record and code state that triggers the failure without stepping through every prior record manually.
def process_records(records: list[dict]) -> list[dict]:
results = []
for record in records:
# Conditional breakpoint here: record.get("amount") is None
# Pauses ONLY when the problematic condition actually occurs,
# skipping potentially thousands of normal records automatically
total = record["amount"] * record["rate"]
results.append({**record, "total": total})
return results