VS Code Debugging Walkthrough¶
Companion guide for 03a_buggy_bmi.py demonstrating VS Code's debugging toolkit: - Setting breakpoints - Using Variables, Watch, and Debug Console panels - Stepping through code - Fixing bugs with runtime inspection
Setup¶
- Open
lectures/01/demo/03a_buggy_bmi.pyin VS Code - Ensure Python extension is installed and correct interpreter selected
- Open Debug panel (bug icon in left sidebar or
Cmd/Ctrl+Shift+D) - Click "Run and Debug" → select "Python File"
Known Bugs¶
This script has THREE intentional bugs: 1. BUG 1: BMI formula error - uses weight / height instead of weight / (height ** 2) 2. BUG 2: NameError - typo catgory vs category in get_bmi_category() 3. BUG 3: IndexError - off-by-one loop error in print_recommendations()
Demonstration Flow¶
BUG 1: Formula Error - Demonstrate Runtime Inspection¶
Set breakpoint: - Click in gutter next to line 21: bmi = weight / height - Red dot appears
Start debugging: - Press F5 or click green play button - Execution pauses at breakpoint
Use Variables panel (left side): - Expand "Locals" section - See weight = 70, height = 1.75 - Note: These look reasonable
Add Watch expression: - Click "+" in Watch panel - Add: weight / (height ** 2) - Shows ~22.9 (correct BMI)
Step Over (F10): - Execute the buggy line - Variables panel updates: bmi = 40.0 (wrong!) - Compare to Watch expression: 40.0 vs 22.9
Debug Console (bottom): - Type: height ** 2 - Shows: 3.0625 (correct square) - Type: weight / height - Shows: 40.0 (what the code actually does)
Fix the bug: - Stop debugger (red square) - Change line 21 to: bmi = weight / (height ** 2)
BUG 2: NameError - Variable Typo¶
Continue execution: - Run debugger again (F5) - Skip through first breakpoint (Continue or F5) - Execution stops with NameError at line 37
Inspect the error: - Variables panel shows: - catgory = "Normal weight" (exists) - Trying to return category (doesn't exist) - Error message points to line 37: return category
Debug Console investigation: - Try: catgory - Shows: "Normal weight" - Try: category - Shows: NameError
Fix the bug: - Change line 37 to: return catgory - OR rename all catgory to category (lines 29-35)
BUG 3: IndexError - Off-by-One Loop¶
Set conditional breakpoint: - Right-click gutter on line 52: for i in range(1, len(category_ids)): - Select "Add Breakpoint" → "Conditional Breakpoint" - Condition: i == len(category_ids) - 1 - This pauses only on the last iteration
Run and observe: - Start debugger (F5) - Pauses on last loop iteration - Variables panel shows: i = 3 (for 4 items)
Add Watch expressions: - len(category_ids) → shows 4 - i → shows 3 - i + 1 → shows 4 (out of bounds!) - recommendations[i+1] → will throw IndexError
Debug Console investigation: - Type: range(1, len(category_ids)) - Shows: range(1, 4) - starts at 1, not 0! - Type: list(range(1, len(category_ids))) - Shows: [1, 2, 3] - skips first item!
Fix the bug: - Change line 52 to: for i in range(len(category_ids)): - Change line 53 to: print(f"{i+1}. {recommendations[i]}") - Use i+1 for display numbering, i for indexing
Final Verification¶
Remove breakpoints: - Click red dots to remove them - Or: "Remove All Breakpoints" in Debug menu
Run normally: - Run script without debugger: python 03a_buggy_bmi.py - Should complete without errors - Should print all patient data and all 4 recommendations
Key Debugging Lessons¶
VS Code debugging panels are powerful: - Variables panel: Automatically shows all local and global variables - Watch panel: Track custom expressions that update each step - Debug Console: Execute any Python code while paused (like a REPL)
When to use each: - Print debugging: Quick checks, early stage debugging - Breakpoints: Pause at specific lines to inspect state - Conditional breakpoints: Pause only when condition is true (e.g., specific iteration) - Watches: Track changing values across multiple steps - Debug Console: Investigate hypotheses ("what if I try...?")
Step commands: - Step Over (F10): Execute current line, don't enter functions - Step Into (F11): Enter function calls to debug them - Step Out (Shift+F11): Finish current function, return to caller - Continue (F5): Run until next breakpoint
Always restart kernel/clean state after debugging: - Restart ensures fixes work from clean slate - Run All to verify end-to-end success
Part C: Writing Tests to Lock In Fixes¶
After debugging and fixing bugs, write tests to ensure they stay fixed. This is how assignments are autograded.
Why Write Tests?¶
Tests prevent regressions: - You fix a bug, but later changes might break it again - Tests catch this immediately - Assignments use pytest to autograde your work
Tests document expected behavior: - Shows what "correct" means - Helps others understand your code
Creating a Test File¶
File: test_03a_bmi.py (already created in demo folder)
Tests follow a pattern: 1. Import the functions you want to test 2. Write functions starting with test_ 3. Use assert to check expected behavior 4. Run with pytest
Test Structure Example¶
def test_bmi_calculation():
"""Test BMI formula is correct."""
bmi = calculate_bmi(70, 1.75) # 70 kg, 1.75 m
assert 22 < bmi < 23, f"Expected BMI ~22.9, got {bmi}"
def test_bmi_category():
"""Test categorization logic."""
category = get_bmi_category(22.0)
assert category == "Normal weight"
Running Tests¶
From command line:
# Run all tests in file
pytest test_03a_bmi.py -v
# Run specific test
pytest test_03a_bmi.py::test_bmi_calculation -v
# Run all tests in directory
pytest -v
In VS Code: 1. Open Testing panel (beaker icon in sidebar) 2. Click "Configure Python Tests" → select pytest 3. Tests appear in tree view 4. Click play button next to test to run it 5. Green checkmark = pass, red X = fail
Test Output Example¶
test_03a_bmi.py::TestCalculateBMI::test_normal_weight_calculation PASSED
test_03a_bmi.py::TestCalculateBMI::test_bmi_bounds PASSED
test_03a_bmi.py::TestGetBMICategory::test_normal_weight_category PASSED
test_03a_bmi.py::TestGetBMICategory::test_boundary_values PASSED
==== 4 passed in 0.03s ====
Connection to Assignments¶
Assignment autograding works the same way: - Your code is in a file (e.g., assignment.py) - Tests are in .github/tests/test_assignment.py - GitHub Actions runs pytest when you push - Green checkmark on GitHub = tests pass = assignment complete
Example assignment test:
def test_student_function():
"""Test student's implementation."""
from assignment import process_data
result = process_data("test_input.csv")
assert len(result) == 50
assert result["bmi"].between(15, 50).all()
Writing Your Own Tests¶
Good test practices: - Test normal cases (typical inputs) - Test boundary cases (edge values like 0, empty, very large) - Test error cases (invalid inputs) - Use descriptive test names - One assertion per test (easier to debug failures)
Example for notebook Demo 3b:
def test_elderly_filter_after_fix():
"""Test age filtering works with numeric types."""
import pandas as pd
patients = pd.DataFrame({
"age": ["70", "30", "80"], # String ages
"name": ["Alice", "Bob", "Carol"]
})
# This was BUG 1 - string comparison doesn't work
# Fix: convert to numeric first
patients["age"] = pd.to_numeric(patients["age"])
elderly = patients[patients["age"] > 65]
assert len(elderly) == 2, "Should find 2 elderly patients"
Running Tests Before Committing¶
Best practice workflow: 1. Fix bug using debugger 2. Write test to verify fix 3. Run test: pytest test_file.py -v 4. Commit if tests pass 5. Push to GitHub (triggers autograding)
Quick pre-commit check:
Summary¶
Debugging workflow with tests: 1. Bug appears → Use debugger to find root cause 2. Fix bug → Change code based on inspection 3. Write test → Lock in the fix with pytest 4. Run test → Verify fix works 5. Commit → Tests prevent future regressions
This is the same workflow used for assignment autograding: - You write/fix code - GitHub Actions runs pytest - Tests pass = assignment complete