Python Basics — Debugging Techniques
Debugging Techniques
Bugs are inevitable. What separates experienced developers from beginners isn’t that they write bug-free code — it’s that they know how to find and fix bugs quickly.
print() Debugging
The simplest and most widely used approach:
def calculate_average(scores):
print(f"DEBUG: scores = {scores}") # What did we receive?
total = sum(scores)
print(f"DEBUG: total = {total}") # Is the sum correct?
count = len(scores)
print(f"DEBUG: count = {count}") # How many items?
average = total / count
print(f"DEBUG: average = {average}") # Final result
return average
result = calculate_average([85, 92, 78, 90, 88])
Tips for print() Debugging
# Label your prints clearly
print(f">>> BEFORE loop: items = {items}")
print(f">>> AFTER loop: results = {results}")
# Print types when confused
print(f"type(x) = {type(x)}, value = {x!r}")
# Use repr (!r) to see exact values including hidden characters
text = "hello\n"
print(f"text = {text!r}") # text = 'hello\n'
# Quick separator for readability
print("=" * 50)
print(f"Processing user: {user}")
print("=" * 50)
When to Remove print() Statements
Always clean up debug prints before committing code. A better approach is to use the logging module (covered below).
Python Debugger (pdb / breakpoint())
The built-in pdb debugger lets you pause execution and inspect your program interactively.
Using breakpoint() (Python 3.7+)
def find_bug(numbers):
total = 0
for i, num in enumerate(numbers):
breakpoint() # Execution pauses here
total += num
return total / len(numbers)
find_bug([10, 20, 30])
When execution hits breakpoint(), you get an interactive prompt:
> /path/to/script.py(5)find_bug()
-> total += num
(Pdb)
Essential pdb Commands
| Command | Short | What It Does |
|---|---|---|
next |
n |
Execute next line (step over) |
step |
s |
Step into function call |
continue |
c |
Continue until next breakpoint |
print(expr) |
p expr |
Print value of expression |
list |
l |
Show current code context |
where |
w |
Show call stack |
quit |
q |
Exit debugger |
break N |
b N |
Set breakpoint at line N |
clear |
cl |
Clear breakpoints |
pdb Session Example
(Pdb) p total # Print current total
0
(Pdb) p num # Print current number
10
(Pdb) p i # Print loop index
0
(Pdb) n # Execute next line
(Pdb) p total # Total after addition
10
(Pdb) c # Continue to next breakpoint
Setting Breakpoints with pdb
import pdb
def process(data):
pdb.set_trace() # Old-style breakpoint (pre-3.7)
# Or: breakpoint() # Modern style (3.7+)
result = transform(data)
return result
VS Code Debugging
VS Code provides a visual debugger that’s easier to use than pdb:
- Set a breakpoint — click the red dot next to a line number
- Start debugging — press
F5or click “Run and Debug” - Inspect variables — hover over variables or check the Variables panel
- Step through code — use the debug toolbar (Step Over, Step Into, Step Out)
VS Code Debug Controls
| Button | Key | Action |
|---|---|---|
| Continue | F5 |
Run until next breakpoint |
| Step Over | F10 |
Execute line, skip into functions |
| Step Into | F11 |
Step into function calls |
| Step Out | Shift+F11 |
Finish current function, return to caller |
| Restart | Ctrl+Shift+F5 |
Restart debugging |
| Stop | Shift+F5 |
Stop debugging |
Logging Module Basics
The logging module is the professional alternative to print() debugging:
import logging
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
)
def process_order(order):
logging.info(f"Processing order #{order['id']}")
if order["total"] > 1000:
logging.warning(f"Large order: ${order['total']}")
try:
charge_payment(order)
logging.info(f"Payment successful for order #{order['id']}")
except PaymentError as e:
logging.error(f"Payment failed: {e}")
raise
Logging Levels
| Level | When to Use | Example |
|---|---|---|
DEBUG |
Detailed diagnostic info | Variable values, flow tracking |
INFO |
Normal operation events | “Server started”, “User logged in” |
WARNING |
Something unexpected but handled | “Disk 85% full” |
ERROR |
Something failed | “Database connection failed” |
CRITICAL |
Program might not continue | “Out of memory” |
Logging to a File
import logging
logging.basicConfig(
filename="app.log",
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logging.info("Application started")
logging.warning("Config file missing, using defaults")
logging.error("Failed to connect to database")
Assert Statements
Use assert to verify assumptions during development:
def calculate_discount(price, percentage):
assert price >= 0, f"Price cannot be negative: {price}"
assert 0 <= percentage <= 100, f"Percentage must be 0-100: {percentage}"
discount = price * (percentage / 100)
final = price - discount
assert final >= 0, "Final price should never be negative"
return final
# Works fine
print(calculate_discount(100, 20)) # 80.0
# Triggers assertion
calculate_discount(-50, 20)
# AssertionError: Price cannot be negative: -50
Important:
assertstatements are removed when Python runs with-O(optimize) flag. Never use them for validation of user input — useif/raiseinstead.
# ❌ Don't use assert for user input validation
assert age > 0, "Age must be positive" # Removed with python -O
# ✅ Use raise for user input validation
if age <= 0:
raise ValueError("Age must be positive")
Common Debugging Strategies
1. Reproduce the Bug
# Write a minimal test case that triggers the bug
def test_bug():
result = buggy_function([1, 2, 3]) # Exact input that fails
expected = 6
assert result == expected, f"Got {result}, expected {expected}"
2. Binary Search Debugging
Comment out half the code. If the bug disappears, it’s in the commented half. Keep halving until you find it.
3. Rubber Duck Debugging
Explain your code line by line to someone (or a rubber duck). The act of explaining often reveals the bug.
4. Check the Obvious First
# Common culprits:
# 1. Off-by-one errors
for i in range(len(items)): # Correct
for i in range(len(items) - 1): # Missing last item!
# 2. Mutable default arguments
def bad(items=[]): # ❌ Shared between calls
def good(items=None): # ✅ Fresh list each call
items = items or []
# 3. Variable shadowing
total = 100
def calc():
total = total + 1 # UnboundLocalError!
return total
# 4. Forgetting return
def add(a, b):
result = a + b
# forgot: return result → returns None
5. Read the Error Message Carefully
# The error message usually tells you EXACTLY what's wrong
# TypeError: can only concatenate str (not "int") to str
# → You need str(number) or f-string
# KeyError: 'username'
# → The key 'username' doesn't exist in the dict
# IndentationError: unexpected indent
# → Check your spaces/tabs are consistent
Summary
| Technique | Best For |
|---|---|
print() |
Quick checks, simple scripts |
breakpoint() / pdb |
Interactive exploration, complex bugs |
| VS Code Debugger | Visual debugging, large projects |
logging |
Production code, long-running programs |
assert |
Development-time sanity checks |
- Start with the error message — it usually points to the problem
- Use
print()for quick debugging,loggingfor production - Learn
breakpoint()and pdb — they’ll save you hours - Check common mistakes: off-by-one, mutable defaults, missing returns
- Write minimal test cases that reproduce the bug
Next up: Error Handling Exercises →