Skip to content

Debug Python in VS Code

Beginner · 12 min · VS Code with the Python extension

Adding print() calls, re-running, adding more print() calls... it works, slowly. A debugger pauses your program on any line and lets you look at every variable, run expressions and step forward one line at a time. Ten minutes learning it pays back on the first real bug.

You need the Python extension from Microsoft (it installs the Python Debugger extension too). Pick your virtual environment with Ctrl+Shift+P → Python: Select Interpreter.


The bug we'll find

This chunker should split a document into overlapping 20-word chunks for RAG. A 50-word document should produce chunks covering all 50 words:

chunker.py
def chunk(text: str, size: int = 20, overlap: int = 5) -> list[str]:
    words = text.split()
    chunks = []
    step = size - overlap
    for start in range(0, len(words) - size, step):
        chunks.append(" ".join(words[start:start + size]))
    return chunks

doc = " ".join(f"w{i}" for i in range(50))
chunks = chunk(doc)
print(len(chunks), "chunks; last one ends with:", chunks[-1].split()[-1])
2 chunks; last one ends with: w34

Words w35 to w49 are silently missing. In a real RAG app, that's the end of every document never being retrievable, and nobody notices until a user asks about it.


1. Set a breakpoint and start debugging

  1. Click in the gutter left of line 5 (the for line). A red dot appears. (Or put the cursor there and press F9.)
  2. Press F5 and choose Python Debugger → Python File.

The program runs and pauses on line 5, before it executes. The Run and Debug sidebar shows:

  • Variables: text, words, chunks, step with their current values.
  • Watch: expressions you want to keep an eye on.
  • Call Stack: how you got here.

2. Ask questions in the Debug Console

Open the Debug Console (Ctrl+Shift+Y) and type any Python expression. It runs in the paused program:

len(words), size, step
(50, 20, 15)
list(range(0, len(words) - size, step))
[0, 15]

There it is: the loop only starts chunks at word 0 and word 15. range(0, 50 - 20, 15) stops before 30, so the chunk that would start at word 30 (covering w30-w49) is never made. The - size was meant to avoid a short last chunk, but it drops the tail of the document.

Add start to Watch and press F10 a few times to watch it take the values 0, then 15, then leave the loop.


3. Fix it and confirm

    for start in range(0, len(words), step):
        chunks.append(" ".join(words[start:start + size]))
        if start + size >= len(words):          # this chunk reached the end: stop
            break
3 chunks; last one ends with: w49

Then add a test so it can't come back (see Write your first tests for an LLM app).


4. The controls

Key Action
F9 Toggle a breakpoint on the current line
F5 Start debugging / continue to the next breakpoint
F10 Step over: run this line, stop at the next
F11 Step into: go inside the function called on this line
Shift+F11 Step out: finish this function, stop in the caller
Ctrl+Shift+F5 Restart
Shift+F5 Stop

Use step over most of the time and step into only for your own functions. To step into library code too (say, to see what LangChain is doing), set "justMyCode": false in launch.json.


5. Smarter breakpoints

Right-click the red dot → Edit Breakpoint:

  • Conditional breakpoint: pause only when an expression is true, e.g. start > 20 or "refund" in question. Essential inside loops over thousands of documents.
  • Hit count: pause on the 100th time through.
  • Logpoint: print a message like start={start} without pausing and without editing your code. It's a print() you don't have to delete afterwards.

In the Breakpoints panel, tick Raised Exceptions to pause the moment an exception is raised, with all the variables still there to inspect, instead of reading the traceback afterwards.


6. launch.json for real apps

For anything other than "run this file", create .vscode/launch.json (Run and Debug → create a launch.json file) and add configurations:

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Current file",
      "type": "debugpy",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal",
      "envFile": "${workspaceFolder}/.env"
    },
    {
      "name": "FastAPI",
      "type": "debugpy",
      "request": "launch",
      "module": "uvicorn",
      "args": ["app.main:app", "--port", "8000"],
      "envFile": "${workspaceFolder}/.env",
      "jinja": true
    },
    {
      "name": "Streamlit",
      "type": "debugpy",
      "request": "launch",
      "module": "streamlit",
      "args": ["run", "app.py"],
      "envFile": "${workspaceFolder}/.env"
    },
    {
      "name": "Ingest script with arguments",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/scripts/ingest.py",
      "args": ["--source", "docs/", "--chunk-size", "500"],
      "justMyCode": false
    }
  ]
}

Choose a configuration in the Run and Debug dropdown and press F5. For FastAPI, set a breakpoint inside an endpoint, then call it from the browser or curl: the request pauses in VS Code. (Leave out --reload while debugging: the reloader starts a child process the debugger isn't attached to.)

Tests: in the Testing panel (the flask icon), right-click any test → Debug Test. Breakpoints in your code and in the test both work.


7. No VS Code? Use breakpoint()

On a server or in a terminal, put breakpoint() in the code and run normally. Python's built-in debugger, pdb, opens at that line:

> chunker.py(6)chunk()
-> for start in range(0, len(words) - size, step):
(Pdb) p len(words), size, step
(50, 20, 15)
(Pdb) p list(range(0, len(words) - size, step))
[0, 15]
(Pdb) c
pdb command Does
p expr Print an expression
n / s / r Next line / step into / return from function
c Continue
l Show the code around the current line
w Where am I (call stack)
q Quit

Remove breakpoint() before committing. A pre-commit hook or Ruff rule (T100) can catch forgotten ones.


Checklist

  • You can set a breakpoint, press F5 and inspect variables.
  • You use the Debug Console to test a hypothesis before changing code.
  • Conditional breakpoints and logpoints replace temporary print()s.
  • launch.json has a configuration for your app (FastAPI, Streamlit or script) with your .env loaded.
  • Every bug you find with the debugger gets a test.

See also VS Code shortcuts for Python and Read a Python error message.

Next: Dockerize a Python AI app →