How to use debugger in VScode

Big Idea

A debugger is a tool that lets you pause a running program, look at exactly what every variable contains at that moment, and then move forward one line at a time. It turns the trace table you do on paper into something the computer does for you — live, on your actual program, with real values. Learning to use it removes one of the most frustrating parts of debugging: not knowing what is actually happening inside the code.

This article is part of the Debugging section. It covers how to use the debugger built into Visual Studio Code — the editor you use in this course. No extra software is needed. Everything described here is already installed on your MacBook.

The debugger does not replace understanding. It is a tool for seeing the truth about what your program is doing. You still need to read what it shows you and reason about what it means. A debugger used without thinking is no more useful than a thermometer used without knowing what a fever is.

LevelWhat it coversWhen to read it
Basic ideaSetting a breakpoint; starting the debugger; reading the Variables panel; stepping through codeRight now. Follow along with your own program open.
At a deeper levelThe four step controls and when to use each one; the Watch panel; the Debug Console; conditional breakpointsOnce you have successfully paused a program and stepped through at least five lines.
At the deepest levelHow the debugger works; the Call Stack panel; when to use the debugger versus print statementsWhen the basic controls feel natural and you want to use the debugger more deliberately.

1   What the Debugger Does

Basic Idea

When you run a program normally, it executes every line as fast as the computer can process them — hundreds of thousands of lines per second. You cannot see what is happening inside. You only see the final output.

The debugger lets you interrupt this process at any line you choose. The program runs at full speed until it reaches that line, then pauses and waits. While paused, you can:

  • See the current value of every variable in your program
  • Move forward one line at a time, watching values change
  • Step into a function call to see what happens inside it
  • Resume normal speed and let the program run to the next pause point

The line where you tell the debugger to pause is called a breakpoint. You set breakpoints before running, and the debugger pauses automatically when it reaches them.

At a Deeper Level

Think of the relationship between a trace table and the debugger this way:

Trace table (on paper)Debugger (in VS Code)
You read the code and predict what each variable will containThe debugger shows you what each variable actually contains
You advance through the program one row at a time by handYou press a button to advance one line at a time
You write the output column as you goThe Debug Console shows output as lines execute
You can start at any line by scanning to itYou set a breakpoint to start pausing at any line
Works for any program you can readWorks only on a program that can actually run

The two techniques complement each other. Trace tables work before you run the program and help you understand code you are reading. The debugger works on running code and shows you what is actually happening, not what you expect.

At the Deepest Level

VS Code's Python debugger works through the Debug Adapter Protocol (DAP) — a standardised interface that separates the editor's debugging UI from the language-specific debugging logic. When you start a debug session for Python, VS Code launches a debug adapter that communicates with Python's built-in debugging hooks (sys.settrace and sys.setprofile). These hooks allow Python to call a function of your choice before executing each line — which is how the debugger intercepts execution at breakpoints.

This architecture means the VS Code debugger UI is the same regardless of language. Once you know how to use it for Python, the same panels, buttons, and concepts apply when you later debug JavaScript, or any other language VS Code supports.

2   Setting a Breakpoint

Basic Idea

A breakpoint marks a line where you want the program to pause. The program runs at full speed until it reaches that line, then stops and waits for you.

To set a breakpoint in VS Code:

  1. Open your Python file.
  2. Click in the gutter — the narrow strip to the left of the line numbers. A red circle appears. That is your breakpoint.
  3. Click the red circle again to remove it.

You can set breakpoints on as many lines as you like. You can also set them before starting the debugger, or while a debug session is already paused.

Set your breakpoint on the first line that is interesting — usually the first line where you suspect something is going wrong, or the start of the function or loop you want to examine.

At a Deeper Level

A few rules about where breakpoints work and where they do not:

Line typeCan you set a breakpoint here?Note
A line with a statement (x = 5, print(), return)YesThe program pauses before this line executes
A blank line or commentVS Code allows it, but the debugger moves it to the next executable lineBetter to set it on the actual statement
A def lineYes, but only pauses when the function is called — not when Python reads the definitionUsually more useful to set the breakpoint on the first line inside the function
A line inside a loopYesThe debugger pauses on every iteration — useful for watching values change across iterations
Set the breakpoint before the problem, not on it

If you suspect the bug is on line 15, set your breakpoint on line 10 or 12 — a few lines earlier. Start with variables in a known good state and step forward. If you set the breakpoint right on the crashing line, you see the broken state but not the path that led there.

At the Deepest Level

Internally, VS Code stores breakpoints as line numbers in a file path. When the debug adapter starts a session, it communicates these to Python's trace function, which intercepts execution before each line and checks whether a breakpoint is registered for that line in that file. If yes, it suspends execution and notifies VS Code, which then updates the UI — highlighting the line, populating the Variables panel, and waiting for a step command.

This interception has a small performance cost: code running under a debugger is measurably slower than code running normally. For the programs you write in Grade 9, this is imperceptible. For large production programs, it can matter.

3   Starting the Debugger

Basic Idea

Once you have set a breakpoint, start the debugger. VS Code will launch your program and pause it automatically when it reaches the breakpoint.

Two ways to start:

  • Press F5
  • Go to the menu: RunStart Debugging

The first time you start debugging a Python file, VS Code may ask you to select a debug configuration. Choose Python File. VS Code remembers this choice for your project.

When the program reaches your breakpoint, everything pauses. A yellow arrow appears on the breakpoint line. The line is highlighted. This is the line that is about to run — it has not executed yet.

The debugging toolbar appears at the top of the screen. You are now in control.

At a Deeper Level

When VS Code asks for a debug configuration, it creates a file called launch.json inside a .vscode folder in your project. This file stores settings for how to launch and debug your program. The default Python File configuration runs whatever file is currently open. You do not need to edit this file for basic use.

If your program takes input from the user (input()), the Debug Console at the bottom of the screen is where you type that input while debugging. It behaves exactly like the terminal for this purpose.

If your program crashes before reaching the breakpoint — because of a syntax error or an earlier runtime error — the debugger will stop at the crash and show you the error, just as if you had run it normally. This is useful: the debugger makes crashes and breakpoints visible in the same interface.

The highlighted line has not run yet

When the debugger pauses, the highlighted line is the next line to execute. The Variables panel shows the state of all variables as they exist just before that line runs. Keep this in mind when reading variable values: if you are paused on x = x + 1, the Variables panel still shows the old value of x. After you step forward, it will show the new value.

At the Deepest Level

The launch.json configuration also supports more advanced options: passing command-line arguments to your program, setting environment variables, specifying a working directory, or attaching to an already-running process rather than launching a new one. For Flask applications, the configuration looks slightly different — you launch the Flask development server under the debugger rather than a standalone Python file. This is covered in the web development section of the knowledge base.

4   Reading the Panels

Basic Idea

When the debugger is paused, the left side of VS Code shows the debugging panels. These replace the normal file explorer for the duration of the debug session. There are four panels. You will use two of them constantly.

Variables
Shows every variable currently in scope and its exact value. This is the most important panel.
Watch
Shows expressions you have chosen to monitor — useful for tracking a specific calculation across many steps.
Call Stack
Shows which function called which. Tells you where you are in the chain of function calls.
Breakpoints
Lists all breakpoints you have set. You can enable and disable them here without removing them.

Start with the Variables panel. It answers the question you always want answered when debugging: what does this variable actually contain right now?

At a Deeper Level

The Variables panel is divided into sections. The most important is Locals — variables in the current scope (the current function, or the main program if you are not inside a function). There is also a Globals section for variables at module level, and a Special Variables section that contains Python internals you can usually ignore.

Lists and dictionaries have a triangle you can click to expand. This shows you every item in the collection — including the index or key and the value. For a list of ten items, expanding it shows all ten. For a nested dictionary, you can expand each key to see its value. This is far faster than writing a print() statement for every item.

The Watch panel lets you type any valid Python expression and see its value update as you step through code. For example, you could watch len(scores) to monitor how long a list is getting, or total / count to see an intermediate calculation without adding a print statement. Click the + button in the Watch panel to add an expression.

Hover over any variable in the editor to see its value

While paused, you can hover your mouse cursor over any variable name in the code editor and VS Code will show a tooltip with its current value. This is often faster than finding it in the Variables panel. For a list or dictionary, the tooltip shows a preview of the contents.

At the Deepest Level

The Call Stack panel shows the current state of the call stack — one entry per active function call, from the outermost at the bottom to the current frame at the top. You can click on any frame in the Call Stack panel to inspect the variables in that frame. This is how you move between frames during debugging: clicking on a stack frame in the panel switches the Variables panel to show that frame's local variables instead of the current frame's.

This is the interactive version of the multi-frame traceback you read in the error messages article. Where the traceback is a printed snapshot of the call stack at the moment of a crash, the Call Stack panel is a live, navigable view of the same structure while the program is paused. You can move up and down the stack, reading each frame's variables, to understand how a particular state came to exist.

5   Stepping Through Code

Basic Idea

Once paused, you move through the program using the debugging toolbar at the top of the screen. There are four buttons you need to know. Three of them move forward by one step. One resumes normal speed.

ButtonNameKeyboardWhat it does
ContinueF5Resumes the program at full speed until it reaches the next breakpoint, or finishes
Step OverF10Executes the current line and pauses on the next one. If the current line calls a function, the whole function runs without pausing inside it.
Step IntoF11Executes the current line. If the current line calls a function, pauses on the first line inside that function.
Step Out⇧F11Runs the rest of the current function at full speed and pauses on the line that called it, after the call returns.

For most debugging, you will use Step Over (F10) most of the time, and Step Into (F11) when you need to see what is happening inside a function.

At a Deeper Level

Choosing between Step Over and Step Into is a judgment call. Here is how to think about it:

SituationUseWhy
The current line calls a function you wrote and you are not sure if that function is correctStep IntoYou want to see what is happening inside the function
The current line calls a built-in function like print(), len(), or int()Step OverStepping into built-in functions takes you into Python internals you cannot read or use
You are inside a function and have seen what you needed to seeStep OutJumps back to the calling code without stepping through every remaining line in the function
You are inside a long loop and want to skip to after itSet a new breakpoint after the loop, then ContinueStepping over every loop iteration one at a time is slow; a breakpoint after the loop jumps directly there
You want to get to a specific line quicklyRight-click the line → Run to CursorRuns at full speed to that line without needing to set a permanent breakpoint
Do not step into library code

If you accidentally step into a Python built-in or library function, you will find yourself looking at compiled C code or complex internal Python that is not helpful to read. Press ⇧F11 (Step Out) to get back to your code immediately. If you end up deep inside library internals, press F5 (Continue) to resume at the next breakpoint, or set a new breakpoint in your own code first.

At the Deepest Level

The four controls map directly to the granularity of control available through Python's tracing hooks. Step Over tells the debug adapter to advance one bytecode instruction at the current scope level. Step Into tells it to advance one instruction at any scope level, including descending into called functions. Step Out tells it to run until the current frame is popped from the stack and execution returns to the calling frame. Continue disables per-line interception entirely until a breakpoint is hit.

VS Code also supports Run to Cursor (right-click any line → Run to Cursor), which temporarily sets a one-time breakpoint at the clicked line, continues execution, and then removes the breakpoint after pausing. This is faster than setting and removing a breakpoint manually when you want to jump to a specific line once.

6   A Complete Debugging Session — Worked Example

Basic Idea

Here is a program with a bug. The intended behaviour is to calculate the average of a list of scores. Read it, try to spot the bug, and then follow the debugger session below to see how you would find it using the debugger.

def calculate_average(scores):
    total = 0
    for score in scores:
        total = total + score
    average = total / len(scores)
    return average

results = [85, 90, 78, 92, 88]
results.append(input("Enter one more score: "))
print("Average:", calculate_average(results))

Running this program produces a TypeError. The bug might not be obvious just from reading. Here is how to find it with the debugger.

At a Deeper Level

Step 1: Set a breakpoint.

Click in the gutter on line 8 — results = [85, 90, 78, 92, 88]. A red circle appears. This is before the interesting code starts.

Step 2: Start the debugger.

Press F5. The program starts and immediately pauses on line 8. The Variables panel shows nothing yet — line 8 has not run.

Step 3: Step Over to line 9.

Press F10. Line 8 executes. The Variables panel now shows: results = [85, 90, 78, 92, 88]. The program pauses on line 9. The input prompt appears in the Debug Console. Type 95 and press Enter.

Step 4: Step Over to line 10.

Press F10. Line 9 executes. Look at the Variables panel: results = [85, 90, 78, 92, 88, '95']. The new item is '95' — a string, not an integer. The quotes give it away. input() always returns a string, and the code did not convert it with int().

Step 5: Confirm the diagnosis.

You do not need to step further. The bug is on line 9: results.append(input(...)) should be results.append(int(input(...))). The string '95' cannot be added to an integer in the loop inside calculate_average, which is where the TypeError would eventually crash.

Step 6: Stop the debugger and fix the bug.

Click the red square Stop button in the toolbar, or press ⇧F5. Fix line 9. Run the debugger again to confirm the fix.

The debugger showed the bug in four steps

Without the debugger, the error message would point to the line inside calculate_average where the addition fails — inside the loop, where a string meets an integer. You would then need to trace backward to understand where the string came from. The debugger let you see the contents of results before passing it into the function, which made the cause immediately visible.

At the Deepest Level

This worked example illustrates a general principle: the most useful moment to inspect a variable is just before it is passed to a function that crashes. The crash site is inside the function, but the root cause is in the data being passed in. Setting the breakpoint before the function call and checking the arguments in the Variables panel is faster than trying to reason about the crash from the error message alone.

This is the technique professionals call narrowing the problem: identify the boundary between where data is correct and where it is wrong, then inspect that boundary. The debugger makes boundary inspection fast and precise.

7   Conditional Breakpoints

Basic Idea

A normal breakpoint pauses the program every time that line is reached. Inside a loop that runs 50 times, the debugger will pause 50 times — which is tedious if you only care about iteration 47.

A conditional breakpoint pauses only when a condition you specify is true. You set one like this:

  1. Right-click the red circle on an existing breakpoint (or right-click in the gutter where you want one).
  2. Choose Edit Breakpoint (or Add Conditional Breakpoint).
  3. Type a Python expression in the box that appears — for example, i == 47 or score < 0.
  4. Press Enter. The breakpoint circle gains a small equals sign to indicate it is conditional.

The debugger now only pauses at that line when the condition is true. The loop runs at full speed for iterations where the condition is false.

At a Deeper Level

Conditional breakpoints are most useful in two situations:

A loop with many iterations where the bug only appears on certain iterations. If your list has 200 items and you suspect the bug happens when the value is negative, set a conditional breakpoint with value < 0. The debugger jumps straight to the first negative value without you pressing F10 two hundred times.

A function called many times where the bug only occurs with specific arguments. Set a conditional breakpoint on the first line of the function with argument_name == "the_bad_value". Only the problematic call will pause.

The condition can be any valid Python expression — comparisons, membership tests (item in list), boolean combinations (x > 10 and y < 0), or even function calls.

Conditional breakpoints are free to set and remove

There is no cost to experimenting with conditional breakpoints. Set them, run the program, and if they do not pause where you expected, adjust the condition or the line. You can have multiple conditional breakpoints active at the same time, and each one has its own independent condition. Remove them by clicking the circle or right-clicking and choosing Remove Breakpoint.

At the Deepest Level

VS Code also supports logpoints — a variant of breakpoints that do not pause execution but instead print a message to the Debug Console when reached. Right-click in the gutter and choose Add Logpoint. In the message box, you can embed expressions in curly braces: Iteration {i}: total is {total}. VS Code evaluates the expressions and prints the result each time the line is reached.

Logpoints are a non-invasive alternative to adding print() statements: they produce output without modifying your code, and they are automatically removed when you stop the debug session. For programs that are difficult to pause mid-execution (web servers, event-driven programs), logpoints are often more practical than breakpoints.

8   When to Use the Debugger vs print()

Basic Idea

You already know how to add print() statements to investigate what your program is doing. The debugger does the same job but differently. Neither tool is always better — knowing when to reach for each one is part of debugging skill.

 print() statementsDebugger
Setup timeAlmost none — just add a lineA few seconds to set a breakpoint and press F5
What you seeOnly what you asked to print, when you askedEvery variable in scope, at any moment you choose
Changing what you inspectEdit the file, run againJust look at a different variable or hover over it
Cleanup requiredYes — you must remove print statements before submissionNo — breakpoints are not part of your code
Works inside loopsYes, but prints every iterationYes, and conditional breakpoints let you pause only on specific iterations
Best forQuick checks on a specific value; programs you know well; situations where you know exactly what to look atBugs you cannot locate; understanding unfamiliar code; inspecting complex data structures; function call chains
At a Deeper Level

A practical rule: start with print() when you have a clear hypothesis about where the bug is and just need to confirm one value. Switch to the debugger when you are genuinely lost — when you do not know which of three functions is producing the wrong value, or when the data structure has become complex enough that printing it is unwieldy.

The debugger is also better when you are reading someone else's code — or AI-generated code — and want to understand how it works. Run it under the debugger, step through it slowly, and watch the Variables panel. This is the mechanical equivalent of doing a trace table, and it is much faster for longer programs.

One situation where print() is clearly better: when you need to see a pattern across many iterations — for example, you want to see how a variable changes across every iteration of a 20-step loop. Printing produces a scrollable history. The debugger requires you to step through and read each iteration individually.

At the Deepest Level

The choice between print() and a debugger has a long history in programming culture. Unix-era programmers often prefer print() debugging (sometimes called printf debugging after C's print function) for its speed and portability — it works anywhere, even in production systems where attaching a debugger is not possible. Debugger advocates point out that print() requires you to know what to look for in advance, while a debugger lets you explore freely.

The practical answer, used by most working programmers, is both — whichever is faster for the specific problem at hand. The important thing is having both available and knowing what each one is good for. Programmers who only know print() are slower on complex bugs; programmers who only use the debugger sometimes reach for a powerful tool when a single print() would have solved it in ten seconds.

Check Your Understanding
  1. In your own words, what is a breakpoint and what happens when the debugger reaches one?
  2. Describe the difference between Step Over (F10) and Step Into (F11). Give one situation where you would choose each.
  3. You are stepping through this program in the debugger. You are paused on line 4 (total = total + score) at the start of the third iteration. Without running the program, what values do you expect to see in the Variables panel for total, score, and scores?

    scores = [10, 20, 30, 40]
    total = 0
    for score in scores:
        total = total + score
    print(total)
  4. You set a breakpoint inside a loop that runs 100 times. After pressing F10 three times, you realise you want to jump to iteration 80. Describe two ways you could do this.
  5. A student opens the Variables panel while paused on the line x = x + 1 and sees x = 5. They step over the line and check again. What value do they expect to see for x now, and why?
  6. You step into a built-in function and find yourself looking at Python internals you cannot read. What do you press to get back to your own code?
  7. Explain in two or three sentences when you would use a conditional breakpoint rather than a normal one. Give a concrete example of a condition you might set.
  8. Name one situation where adding a print() statement is faster than using the debugger, and one situation where the debugger is clearly better. Explain your reasoning for each.