Basic Debugging Techniques
Learn to find and fix bugs like a professional developer
Introduction
Every programmer, from beginners to experts, writes buggy code. The difference between struggling for hours and fixing issues in minutes is knowing how to debug effectively. Debugging is a critical skill that separates hobbyists from professionals. In this lesson, you'll learn practical debugging techniques using Python's built-in tools: strategic print statements, understanding error messages, reading stack traces, and systematic approaches to finding bugs. These fundamental techniques will save you countless hours and make you a more confident developer.
Using print() Effectively
The humble print() statement is the most fundamental debugging tool. Used strategically, it's incredibly powerful for understanding what your code is actually doing.
Basic Print Debugging
Add print statements to track variable values and program flow.
# Example: Find the bug
def calculate_total(prices, tax_rate):
subtotal = sum(prices)
tax = subtotal * tax_rate
total = subtotal + tax
return total
prices = [10, 20, 30]
tax_rate = 0.08
result = calculate_total(prices, tax_rate)
print(f"Total: {result}") # Total: 64.8
# Add debugging prints to understand the flow
def calculate_total(prices, tax_rate):
print(f"DEBUG: prices = {prices}")
subtotal = sum(prices)
print(f"DEBUG: subtotal = {subtotal}")
tax = subtotal * tax_rate
print(f"DEBUG: tax = {tax}")
total = subtotal + tax
print(f"DEBUG: total = {total}")
return total
# Output:
# DEBUG: prices = [10, 20, 30]
# DEBUG: subtotal = 60
# DEBUG: tax = 4.8
# DEBUG: total = 64.8
# Total: 64.8Print Variable Types
Many bugs come from unexpected data types. Always check types when debugging.
# Bug: Why doesn't this math work?
def calculate_discount(price, discount):
result = price - discount
return result
price = "100" # Oops! String instead of int
discount = 10
# Add type checking
print(f"price = {price}, type = {type(price)}")
# price = 100, type = <class 'str'>
print(f"discount = {discount}, type = {type(discount)}")
# discount = 10, type = <class 'int'>
# This will error!
# result = calculate_discount(price, discount)
# TypeError: unsupported operand type(s) for -: 'str' and 'int'
# Fix: Convert to proper type
price = int(price)
result = calculate_discount(price, discount)
print(f"Result: {result}") # Result: 90
# Better debugging format
def debug_print(var_name, var_value):
"""Print variable name, value, and type"""
print(f"{var_name} = {var_value!r} (type: {type(var_value).__name__})")
debug_print("price", price)
# price = 100 (type: int)
debug_print("discount", discount)
# discount = 10 (type: int)Print at Key Points
Add prints at function entry, exit, and decision points to trace execution flow.
def process_user_age(age_str):
"""Process user age input"""
print(f"[ENTRY] process_user_age called with: {age_str!r}")
# Validate input
if not age_str.isdigit():
print("[BRANCH] Invalid input - not a digit")
return None
age = int(age_str)
print(f"[CONVERTED] age_str to int: {age}")
# Check age range
if age < 0:
print("[BRANCH] Age is negative")
return None
elif age > 150:
print("[BRANCH] Age is too high")
return None
else:
print("[BRANCH] Age is valid")
print(f"[EXIT] Returning: {age}")
return age
# Test with different inputs
result1 = process_user_age("25")
# [ENTRY] process_user_age called with: '25'
# [CONVERTED] age_str to int: 25
# [BRANCH] Age is valid
# [EXIT] Returning: 25
print(f"Result 1: {result1}\n")
result2 = process_user_age("abc")
# [ENTRY] process_user_age called with: 'abc'
# [BRANCH] Invalid input - not a digit
print(f"Result 2: {result2}\n")
result3 = process_user_age("-5")
# [ENTRY] process_user_age called with: '-5'
# [BRANCH] Invalid input - not a digit
print(f"Result 3: {result3}")Pretty Print for Complex Data
Use pprint for readable output of complex data structures.
from pprint import pprint
# Complex nested data
users = [
{
"id": 1,
"name": "Alice",
"address": {"city": "NYC", "zip": "10001"},
"scores": [85, 90, 92]
},
{
"id": 2,
"name": "Bob",
"address": {"city": "LA", "zip": "90001"},
"scores": [78, 82, 88]
}
]
# Regular print (hard to read)
print("Regular print:")
print(users)
# [{'id': 1, 'name': 'Alice', 'address': {'city': 'NYC', 'zip': '10001'}, 'scores': [85, 90, 92]}, ...]
# Pretty print (much better!)
print("\nPretty print:")
pprint(users)
# [{'address': {'city': 'NYC', 'zip': '10001'},
# 'id': 1,
# 'name': 'Alice',
# 'scores': [85, 90, 92]},
# {'address': {'city': 'LA', 'zip': '90001'},
# 'id': 2,
# 'name': 'Bob',
# 'scores': [78, 82, 88]}]
# Pretty print with custom width
pprint(users, width=40)
# Pretty print specific items
print("\nFirst user:")
pprint(users[0], width=40)
# {'address': {'city': 'NYC',
# 'zip': '10001'},
# 'id': 1,
# 'name': 'Alice',
# 'scores': [85, 90, 92]}Strategic Debugging Markers
Use clear markers to distinguish debug output and make it easy to find and remove later.
# Use clear prefixes for debug statements
def process_data(data):
print("=" * 50)
print(f"🐛 DEBUG START: process_data")
print("=" * 50)
print(f"🔍 Input data: {data}")
print(f"🔍 Data type: {type(data)}")
print(f"🔍 Data length: {len(data)}")
result = [x * 2 for x in data]
print(f"🔍 Result: {result}")
print("=" * 50)
print(f"✅ DEBUG END: process_data")
print("=" * 50)
return result
output = process_data([1, 2, 3, 4, 5])
# Output:
# ==================================================
# 🐛 DEBUG START: process_data
# ==================================================
# 🔍 Input data: [1, 2, 3, 4, 5]
# 🔍 Data type: <class 'list'>
# 🔍 Data length: 5
# 🔍 Result: [2, 4, 6, 8, 10]
# ==================================================
# ✅ DEBUG END: process_data
# ==================================================
# Create a debug helper function
DEBUG = True # Toggle debugging on/off
def debug_log(message, variable=None):
"""Print debug message only if DEBUG is True"""
if DEBUG:
if variable is not None:
print(f"🐛 {message}: {variable}")
else:
print(f"🐛 {message}")
# Use it in your code
def calculate(x, y):
debug_log("calculate() called")
debug_log("x", x)
debug_log("y", y)
result = x + y
debug_log("result", result)
return result
calculate(10, 20)
# 🐛 calculate() called
# 🐛 x: 10
# 🐛 y: 20
# 🐛 result: 30Remember to remove debug prints! Before committing code, search for your debug markers (like "DEBUG" or "🐛") and remove them. Consider using a logging library for production code instead.
Understanding Error Messages
Python's error messages tell you exactly what went wrong and where. Learning to read them effectively is crucial for fast debugging.
Anatomy of an Error Message
Error messages have three main parts: the traceback, the error type, and the error message.
# Example code with an error
def calculate_average(numbers):
total = sum(numbers)
count = len(numbers)
average = total / count
return average
def process_scores(scores):
avg = calculate_average(scores)
return avg * 1.1
# This will cause an error
result = process_scores([])
# Error output:
"""
Traceback (most recent call last): ← The traceback
File "script.py", line 11, in <module> ← Where error originated
result = process_scores([])
File "script.py", line 8, in process_scores ← Call chain
avg = calculate_average(scores)
File "script.py", line 4, in calculate_average ← Where error occurred
average = total / count
ZeroDivisionError: division by zero ← Error type & message
"""
# How to read this:
# 1. Start from the BOTTOM - that's where the error occurred
# 2. ZeroDivisionError tells you WHAT went wrong
# 3. "division by zero" tells you WHY
# 4. Line 4 tells you WHERE: average = total / count
# 5. The traceback shows the call chain
# Fix: Add validation
def calculate_average(numbers):
if len(numbers) == 0:
return 0 # or raise ValueError("Cannot calculate average of empty list")
total = sum(numbers)
count = len(numbers)
average = total / count
return average
result = process_scores([])
print(result) # 0.0Common Error Types
# 1. SyntaxError - Code structure is wrong
# print("Hello" # Missing closing parenthesis
# SyntaxError: unexpected EOF while parsing
# 2. NameError - Variable doesn't exist
# print(unknown_variable)
# NameError: name 'unknown_variable' is not defined
# 3. TypeError - Wrong type for operation
result = "5" + 10
# TypeError: can only concatenate str (not "int") to str
# Fix:
result = int("5") + 10 # Convert string to int
# or
result = "5" + str(10) # Convert int to string
# 4. ValueError - Right type, wrong value
age = int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'
# 5. IndexError - List index out of range
numbers = [1, 2, 3]
print(numbers[10])
# IndexError: list index out of range
# 6. KeyError - Dictionary key doesn't exist
person = {"name": "Alice"}
print(person["age"])
# KeyError: 'age'
# Fix: Use .get() with default
age = person.get("age", "Unknown")
print(age) # "Unknown"
# 7. AttributeError - Object doesn't have that attribute
text = "hello"
text.append("world")
# AttributeError: 'str' object has no attribute 'append'
# (strings are immutable, can't append)
# 8. ZeroDivisionError - Division by zero
result = 10 / 0
# ZeroDivisionError: division by zero
# 9. IndentationError - Incorrect indentation
# def hello():
# print("Hi") # Not indented
# IndentationError: expected an indented blockReading Stack Traces
Stack traces show the complete call chain. Read from bottom to top to find the root cause.
# Complex example with multiple function calls
def level_1():
print("Level 1 started")
level_2()
print("Level 1 finished")
def level_2():
print("Level 2 started")
level_3()
print("Level 2 finished")
def level_3():
print("Level 3 started")
problem = []
print(problem[0]) # This will error!
print("Level 3 finished")
# Run it
level_1()
# Output:
"""
Level 1 started
Level 2 started
Level 3 started
Traceback (most recent call last):
File "script.py", line 16, in <module>
level_1() ← 1. Entry point
File "script.py", line 3, in level_1
level_2() ← 2. Called level_2()
File "script.py", line 8, in level_2
level_3() ← 3. Called level_3()
File "script.py", line 13, in level_3
print(problem[0]) ← 4. Error here!
IndexError: list index out of range
"""
# How to read:
# 1. Error occurred in level_3() at line 13
# 2. level_3() was called by level_2() at line 8
# 3. level_2() was called by level_1() at line 3
# 4. level_1() was called from main at line 16
# The traceback shows the exact path the code took
# This helps you understand the context of the errorHandling Expected Errors
Use try-except blocks to handle errors gracefully and get better error information.
# Example: User input validation
def get_age():
age_str = input("Enter your age: ")
try:
age = int(age_str)
if age < 0:
print("Error: Age cannot be negative")
return None
elif age > 150:
print("Error: Age seems unrealistic")
return None
return age
except ValueError:
print(f"Error: '{age_str}' is not a valid number")
return None
# Better: Provide specific error messages
def safe_divide(a, b):
"""Divide a by b with error handling"""
print(f"Attempting to divide {a} by {b}")
try:
result = a / b
print(f"Success: {a} / {b} = {result}")
return result
except ZeroDivisionError:
print(f"Error: Cannot divide {a} by zero")
return None
except TypeError as e:
print(f"Error: Invalid types for division")
print(f" a = {a!r} (type: {type(a).__name__})")
print(f" b = {b!r} (type: {type(b).__name__})")
print(f" Original error: {e}")
return None
# Test it
safe_divide(10, 2) # Success: 10 / 2 = 5.0
safe_divide(10, 0) # Error: Cannot divide 10 by zero
safe_divide("10", 2) # Error: Invalid types for division
# Get detailed error information
def detailed_error_handler(func, *args, **kwargs):
"""Run function and print detailed error info if it fails"""
try:
return func(*args, **kwargs)
except Exception as e:
print(f"❌ Error in {func.__name__}()")
print(f" Error type: {type(e).__name__}")
print(f" Error message: {str(e)}")
print(f" Arguments: {args}")
print(f" Keyword arguments: {kwargs}")
return None
# Use it
detailed_error_handler(int, "abc")
# ❌ Error in int()
# Error type: ValueError
# Error message: invalid literal for int() with base 10: 'abc'
# Arguments: ('abc',)
# Keyword arguments: {}Pro tip: When debugging, temporarily catch all exceptions with except Exception as e: to see what's going wrong, then replace it with specific exception types once you know what to expect.
Systematic Debugging Strategies
Use these proven strategies to debug efficiently and avoid getting stuck.
The Scientific Method of Debugging
# 1. REPRODUCE the bug consistently
# 2. ISOLATE where the problem occurs
# 3. UNDERSTAND what should happen vs. what does happen
# 4. HYPOTHESIZE what might be wrong
# 5. TEST your hypothesis
# 6. FIX the bug
# 7. VERIFY the fix works
# Example: Bug in shopping cart
def calculate_cart_total(items):
"""Calculate total price of items in cart"""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
# Bug report: Total is wrong for cart with discount
cart = [
{"name": "Book", "price": 20, "quantity": 2},
{"name": "Pen", "price": 5, "quantity": 3},
{"name": "Discount", "price": -10, "quantity": 1} # Discount as negative
]
# Step 1: REPRODUCE
result = calculate_cart_total(cart)
print(f"Total: ${result}") # Total: $45
# Expected: $45 (20*2 + 5*3 + -10*1 = 40 + 15 - 10 = 45)
# Actual: $45 ✓ Works!
# Let's try another case
cart2 = [
{"name": "Book", "price": 20, "quantity": 2, "discount": 0.1},
{"name": "Pen", "price": 5, "quantity": 3, "discount": 0}
]
# Step 2: ISOLATE - Add debug prints
def calculate_cart_total_debug(items):
total = 0
print(f"Starting calculation with {len(items)} items")
for i, item in enumerate(items):
print(f"\n Item {i}: {item['name']}")
print(f" price: {item['price']}")
print(f" quantity: {item['quantity']}")
item_total = item["price"] * item["quantity"]
print(f" item_total: {item_total}")
total += item_total
print(f" running total: {total}")
return total
# Step 3: UNDERSTAND what's wrong
result = calculate_cart_total_debug(cart2)
# Total is $55, but expected $51 (Book has 10% discount!)
# Step 4: HYPOTHESIZE
# The function ignores the 'discount' field entirely
# Step 5: TEST hypothesis
# Verify that applying discounts produces the correct total
# Step 6: FIX
def calculate_cart_total_fixed(items):
total = 0
for item in items:
price = item["price"]
quantity = item["quantity"]
discount = item.get("discount", 0) # Default to 0 if missing
item_total = price * quantity * (1 - discount)
total += item_total
return total
# Step 7: VERIFY
result = calculate_cart_total_fixed(cart2)
print(f"\nFixed total: ${result}") # $51.0 (40*0.9 + 15)
# Expected: Book with 10% discount: 20*2*0.9 = 36
# Pen no discount: 5*3 = 15
# Total: 36 + 15 = 51 ✓Binary Search Debugging
When you have a large codebase, use binary search to quickly narrow down where the bug is.
# Long function with a bug somewhere
def process_user_data(user_input):
# Step 1: Parse input
parts = user_input.split(",")
# Step 2: Validate
if len(parts) != 3:
return None
# Step 3: Extract data
name = parts[0].strip()
age_str = parts[1].strip()
city = parts[2].strip()
# Step 4: Convert types
age = int(age_str)
# Step 5: Validate age
if age < 0 or age > 150:
return None
# Step 6: Format output
result = {
"name": name.capitalize(),
"age": age,
"city": city.upper()
}
return result
# Bug: Doesn't work with "Alice,30,NYC"
user_input = "Alice,30,NYC"
result = process_user_data(user_input)
print(result) # Something's wrong!
# Binary search approach:
# Add checkpoint in the MIDDLE of the function
def process_user_data(user_input):
parts = user_input.split(",")
if len(parts) != 3:
return None
name = parts[0].strip()
age_str = parts[1].strip()
city = parts[2].strip()
# CHECKPOINT: Middle of function
print(f"🔍 CHECKPOINT: name={name!r}, age_str={age_str!r}, city={city!r}")
age = int(age_str)
if age < 0 or age > 150:
return None
result = {
"name": name.capitalize(),
"age": age,
"city": city.upper()
}
return result
# Run it
result = process_user_data(user_input)
# 🔍 CHECKPOINT: name='Alice', age_str='30', city='NYC'
# Works! ✓
# Bug is AFTER the checkpoint
# Add checkpoint at 3/4 point
def process_user_data(user_input):
parts = user_input.split(",")
if len(parts) != 3:
return None
name = parts[0].strip()
age_str = parts[1].strip()
city = parts[2].strip()
age = int(age_str)
if age < 0 or age > 150:
return None
# CHECKPOINT: Near end
print(f"🔍 CHECKPOINT 2: age={age}, about to format")
result = {
"name": name.capitalize(),
"age": age,
"city": city.upper()
}
print(f"🔍 CHECKPOINT 3: result={result}")
return result
# This binary search approach quickly narrows down the problem area!Rubber Duck Debugging
Explain your code line-by-line to an inanimate object (like a rubber duck). This forces you to think through the logic and often reveals the bug.
# Bug: Function returns wrong value
def count_vowels(text):
vowels = "aeiou"
count = 0
for char in text:
if char in vowels:
count += 1
return count
result = count_vowels("Hello World")
print(result) # Expected 3 (e, o, o), Got 3 ✓
result = count_vowels("HELLO WORLD")
print(result) # Expected 3, Got 0! ❌
# Explain to rubber duck:
"""
Duck: "What does your function do?"
Me: "It counts vowels in text"
Duck: "Walk me through it with 'HELLO'"
Me: "First, I define vowels as 'aeiou'"
"Then for each character in text..."
"Wait - 'H' is uppercase, but vowels only has lowercase!"
Duck: "🦆 There's your bug!"
"""
# Fix: Handle both cases
def count_vowels(text):
vowels = "aeiouAEIOU" # Include uppercase
count = 0
for char in text:
if char in vowels:
count += 1
return count
# Or better: convert to lowercase
def count_vowels(text):
vowels = "aeiou"
count = 0
for char in text.lower(): # Convert to lowercase
if char in vowels:
count += 1
return count
result = count_vowels("HELLO WORLD")
print(result) # 3 ✓
# The act of explaining forces you to think through each step
# and often reveals assumptions you didn't realize you made!Simplify and Test in Isolation
When debugging complex code, extract the problematic part and test it separately.
# Complex function with multiple steps
def analyze_sales_data(filename):
# Read file
with open(filename) as f:
data = f.read()
# Parse CSV
lines = data.strip().split("\n")
headers = lines[0].split(",")
rows = [line.split(",") for line in lines[1:]]
# Calculate totals
total_revenue = 0
for row in rows:
price = float(row[2])
quantity = int(row[3])
total_revenue += price * quantity
# Calculate average
avg_sale = total_revenue / len(rows)
return avg_sale
# Bug somewhere in here! Instead of debugging the whole thing,
# isolate and test each part:
# Step 1: Isolate file reading
def test_file_reading():
with open("sales.csv") as f:
data = f.read()
print(f"Read {len(data)} characters")
print(data[:100]) # First 100 chars
# Step 2: Isolate CSV parsing
def test_csv_parsing():
data = "name,id,price,quantity\nWidget,1,10.50,2\nGadget,2,25.00,1"
lines = data.strip().split("\n")
print(f"Lines: {lines}")
headers = lines[0].split(",")
print(f"Headers: {headers}")
rows = [line.split(",") for line in lines[1:]]
print(f"Rows: {rows}")
# Step 3: Isolate calculation
def test_calculation():
rows = [
["Widget", "1", "10.50", "2"],
["Gadget", "2", "25.00", "1"]
]
total_revenue = 0
for row in rows:
print(f"Processing row: {row}")
price = float(row[2])
quantity = int(row[3])
item_revenue = price * quantity
print(f" price={price}, quantity={quantity}, revenue={item_revenue}")
total_revenue += item_revenue
print(f"Total revenue: {total_revenue}")
avg_sale = total_revenue / len(rows)
print(f"Average sale: {avg_sale}")
# Run isolated tests
test_csv_parsing()
test_calculation()
# This quickly shows which part has the bug!
# Much easier than debugging everything at once.Common Debugging Pitfalls
# Pitfall 1: Mutable default arguments
def add_item(item, items=[]): # DON'T DO THIS!
items.append(item)
return items
# Bug: List persists between calls
list1 = add_item("apple")
print(list1) # ["apple"]
list2 = add_item("banana") # Expected ["banana"]
print(list2) # ["apple", "banana"] ❌ Bug!
# Fix: Use None as default
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
# Pitfall 2: Variable scope confusion
x = 10
def modify():
x = 20 # Creates local variable, doesn't modify global
print(f"Inside: {x}")
modify() # Inside: 20
print(f"Outside: {x}") # Outside: 10 (unchanged!)
# Fix: Use global or return
def modify():
global x
x = 20
# Or better: return the value
def modify(x):
return x + 10
x = modify(x)
# Pitfall 3: Comparing floats
result = 0.1 + 0.2
print(result == 0.3) # False! ❌
# Fix: Use approximate comparison
print(abs(result - 0.3) < 0.0001) # True ✓
# Pitfall 4: Using 'is' instead of '=='
x = 1000
y = 1000
print(x == y) # True (values are equal)
print(x is y) # False (different objects)
# 'is' checks identity, '==' checks equality
# Pitfall 5: Modifying list while iterating
numbers = [1, 2, 3, 4, 5]
for num in numbers:
if num % 2 == 0:
numbers.remove(num) # DON'T DO THIS!
# Fix: Create new list or iterate over copy
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]Prevention is better than debugging: Write small functions, add docstrings, use type hints, write tests, and use meaningful variable names. Good code is easier to debug!
Debugging Checklist
When you're stuck, work through this checklist systematically.
✓ First Steps
- Read the error message carefully
- Look at the line number where error occurred
- Check variable types with print(type(var))
- Verify input data is what you expect
- Make sure you saved the file!
- Restart your Python interpreter
✓ Investigation
- Add print() statements at key points
- Check intermediate values
- Trace execution flow
- Test with simpler input
- Isolate the problematic code
- Comment out code to narrow it down
✓ Common Fixes
- Check indentation (spaces vs tabs)
- Verify parentheses are balanced
- Look for typos in variable names
- Check if variables are defined before use
- Ensure functions return values
- Verify function arguments match
✓ When Still Stuck
- Take a break and come back fresh
- Explain the problem to someone (or rubber duck)
- Search the error message online
- Check Python documentation
- Create minimal reproducible example
- Ask for help with specific details
Key Takeaways
Print Debugging
- Add prints at function entry/exit
- Check variable values and types
- Use clear debug markers (🐛, DEBUG)
- Use pprint for complex data
- Remember to remove debug prints!
Error Messages
- Read from bottom to top
- Error type tells you what went wrong
- Line number tells you where
- Traceback shows the call chain
- Learn common error types
Debugging Strategies
- Reproduce the bug consistently
- Isolate the problem area
- Use binary search for large code
- Test parts in isolation
- Try rubber duck debugging
Best Practices
- Write small, testable functions
- Use meaningful variable names
- Add validation for inputs
- Handle errors gracefully
- Take breaks when stuck!
What's Next?
🎉 Congratulations on completing Python Foundations! You've mastered essential debugging techniques and built a solid foundation in Python. You're now ready to:
- Build real projects - Apply what you've learned by creating applications, automating tasks, or analyzing data
- Continue with Python Intermediate - Explore OOP, file formats (CSV/JSON/XML), testing, regular expressions, and advanced patterns
- Join the Python community - Contribute to open source, share your projects, and keep learning from others
Remember: Every expert was once a beginner. Keep practicing, stay curious, and embrace the bugs - they're opportunities to learn! 🚀