Orchestrating VS Code for Python + Frontend Development
Follow along with all the code examples and configurations
https://github.com/satyamsoni2211/pyconf2026_multi-stack-debug-masteryIn the era of microservices and full-stack applications, debugging in isolation is rarely enough.
"The best debugging tool is still careful thought, coupled with well-designed code."
— Brian Kernighan
Intro → Part 1 → Part 2 → Part 3 → Part 4 → Q&A
By the end of this workshop, you'll be able to:
launch.json Configuration
VS Code provides powerful Python debugging through the Python extension:
{
"name": "Python: Debug File",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"console": "integratedTerminal",
"justMyCode": true,
"env": {
"DEBUG": "true"
}
}
{
"name": "Python: Module",
"type": "debugpy",
"request": "launch",
"module": "fastapi",
"args": [
"dev",
"--port", "8080"
],
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"justMyCode": true
}
{
"name": "Python: Pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": [
"tests/",
"-v",
"-s"
],
"console": "integratedTerminal",
"justMyCode": false,
"env": {
"PYTHONPATH": "${workspaceFolder}"
}
}
{
"name": "Python: Django",
"type": "debugpy",
"request": "launch",
"module": "django",
"args": [
"runserver",
"--noreload"
],
"django": true,
"justMyCode": true,
"env": {
"DJANGO_SETTINGS_MODULE": "mysite.settings"
}
}
JavaScript & Browser Debugging
VS Code supports multiple frontend debugging scenarios:
{
"name": "Chrome: Launch",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true,
"preLaunchTask": "npm: dev",
"runtimeArgs": [
"--disable-web-security"
]
}
{
"name": "Chrome: Attach",
"type": "chrome",
"request": "attach",
"port": 9222,
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true
}
/* Start Chrome with remote debugging:
chrome --remote-debugging-port=9222 */
{
"name": "Node: Launch",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/server/index.js",
"console": "integratedTerminal",
"runtimeExecutable": "node",
"skipFiles": ["<node_internals>/**"]
}
{
"name": "Node: Nodemon",
"type": "node",
"request": "launch",
"runtimeExecutable": "nodemon",
"program": "${workspaceFolder}/server/index.js",
"console": "integratedTerminal"
}
{
"name": "Electron: Main",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"program": "${workspaceFolder}/main.js",
"console": "integratedTerminal"
}
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "launch",
"url": "file://${workspaceFolder}/index.html",
"webRoot": "${workspaceFolder}"
}
Mastering the Debugger
/* Right-click on breakpoint → Edit Condition */
/* Condition: only pause when true */
user.is_admin == true
/* Hit Count: pause after N hits
Format: "> 5" (greater than 5)
"== 10" (equals 10)
"% 3 == 0" (every 3rd hit) */
/* Log Message: print without pausing
Format: "Log: user = " + user.name */
The debug toolbar provides essential controls:
Inspecting Application State
The Variables panel shows current scope state:
The call stack shows execution history:
Execute code in the debug context:
class="code-comment">/* In Debug Console: */
class="code-comment">/* Inspect variables */
user
user.name
user.is_active
class="code-comment">/* Execute code */
print(f"User: {user.name}")
result = calculate_total(items)
class="code-comment">/* Test class="code-keyword">function calls */
response = api.get_user(user_id)
response.json()
class="code-comment">/* Modify values (careful!) */
count = 100
15 minutes
Real-world projects span multiple directories:
Let's create a VS Code task to run FastAPI via uv run:
{
"version": "2.0.0",
"tasks": [
{
"label": "Run FastAPI Server",
"type": "shell",
"command": "uv run fastapi dev --port 8080",
"problemMatcher": [],
"isBackground": false,
"presentation": {
"reveal": "always",
"panel": "new"
}
}
]
}
15 minutes
{
"label": "Start: Vite Dev Server",
"type": "shell",
"command": "npm run dev",
"isBackground": true,
"problemMatcher": [
"$tsc-watch",
{
"owner": "vite",
"pattern": {
"regexp": "^.*$",
"message": 0
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*VITE.*",
"endsPattern": ".*ready in.*"
}
}
]
}
Problem matchers parse CLI output to:
{
"problemMatcher": {
"owner": "fastapi",
"fileLocation": "relative",
"pattern": {
"regexp": "^Application startup complete: (.+)$",
"message": 1
},
"background": {
"activeOnStart": true,
"beginsPattern": "^.*Uvicorn.*$",
"endsPattern": "Application startup complete"
}
}
}
Create a background task that:
- Manual start order
- Guess when services are ready
- Race conditions
- Time wasted waiting
+ Automatic startup sequence
+ Problem matcher ensures ready
+ No race conditions
+ One-click full stack start
15 minutes
For complex managed environments, Attach is superior:
{
"name": "Python: Remote Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}/backend",
"remoteRoot": "/app"
}
],
"justMyCode": true
}
pathMappings bridges local and remote file systems:
Configure remote attach debugging:
20 minutes
Compounds let you launch multiple debug configurations at once:
{
"compounds": [
{
"name": "Full Stack Debug",
"configurations": [
"Python: FastAPI Attach",
"Chrome: Launch Frontend"
],
"stopAll": true
}
],
"configurations": [
{
"name": "Python: FastAPI Attach",
"type": "debugpy",
"request": "attach",
"connect": { "port": 5678 },
"preLaunchTask": "Start FastAPI"
},
{
"name": "Chrome: Launch Frontend",
"type": "chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/frontend"
}
]
}
preLaunchTask runs a task before debugging starts:
// tasks.json
{
"tasks": [
{
"label": "Start: FastAPI (debugpy on :5678)",
"type": "shell",
"command": "uv run debugpy --listen 5678 --wait-for-client -m fastapi dev --port 8081",
"isBackground": true,
"problemMatcher": {
"owner": "fastapi-debugpy",
"background": {
"activeOnStart": true,
"beginsPattern": ".*FastAPI\\s+Starting development server.*",
"endsPattern": ".*Application startup complete.*"
}
}
},
{
"label": "Start: Vite Dev Server",
"type": "shell",
"command": "npm run dev",
"isBackground": true,
"problemMatcher": [
"$tsc-watch",
{
"owner": "vite",
"pattern": {
"regexp": "^.*$",
"message": 0
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*VITE.*",
"endsPattern": ".*ready in.*"
}
}
]
}
]
}
// launch.json - compound
{
"compounds": [
{
"name": "Debug Full Stack",
"configurations": [
"Python: FastAPI Attach",
"JavaScript: Chrome"
],
"stopAll": true
}
]
}
When you stop debugging:
- 3+ terminal tabs open
- Manual start order
- Guess when ready
- Separate debug sessions
- Hard to reproduce issues
+ One click starts all
+ Automatic ordering
+ Problem matchers ensure ready
+ Unified debugger
+ Full stack breakpoints
Create a complete debugging workflow:
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are by definition not smart enough to debug it."
— Brian Kernighan
Let's apply what we've learned:
Questions? | @satyamsoni2211 | PyConf 2026