- Use print statements for debugging
- Understand the Python debugger basics
- Apply systematic debugging techniques
- Prevent bugs with good coding practices
Debugging Basics
Every programmer, from beginner to expert, spends time debugging. A bug is simply code that doesn't behave as expected. Finding and fixing bugs is like being a detective – you gather clues, form hypotheses, and test them until you crack the case!
The good news? Debugging is a skill that improves with practice. Let's learn some powerful techniques!
The Debugging Mindset
The Scientific Method of Debugging
1. OBSERVE: What's actually happening?
"The program crashes when I click submit"
2. REPRODUCE: Can you make it happen consistently?
"It crashes every time with empty input"
3. HYPOTHESIZE: What might be causing it?
"Maybe it's not handling empty strings"
4. TEST: Check your hypothesis
Add print statements or check the code
5. FIX: Make the smallest change that fixes it
6. VERIFY: Confirm the bug is gone
Test with the same inputs that caused the bug
Print Debugging
The simplest and most common technique:
def calculate_average(numbers):
print(f"DEBUG: numbers = {numbers}") # What did we receive?
total = sum(numbers)
print(f"DEBUG: total = {total}") # Is the sum correct?
count = len(numbers)
print(f"DEBUG: count = {count}") # How many items?
average = total / count
print(f"DEBUG: average = {average}") # Final result
return average
# When you find the bug, remove the print statements!
Pro Tips for Print Debugging
# Tip 1: Use prefixes to find your prints easily
print(">>> Starting function")
print(">>> Value:", x)
# Tip 2: Print variable names AND values
print(f"x = {x}, y = {y}, result = {result}")
# Tip 3: Print type when debugging type errors
print(f"data type: {type(data)}, value: {data}")
# Tip 4: Print at decision points
if condition:
print(">>> Taking the if branch")
else:
print(">>> Taking the else branch")
Rubber Duck Debugging
This sounds silly but really works!
Rubber Duck Debugging
"Okay duck, let me explain what this code does..."
The process:
1. Get a rubber duck (or any object, or imaginary friend)
2. Explain your code LINE BY LINE out loud
3. Explain what EACH line is supposed to do
4. Explain what ACTUALLY happens
Why it works:
• Forces you to slow down and think carefully
• You often realize the bug while explaining
• "Wait, that line doesn't make sense because..."
Using the Debugger
IDEs have built-in debuggers that let you:
- Set breakpoints: Pause execution at specific lines
- Step through code: Execute one line at a time
- Inspect variables: See all variable values
- Watch expressions: Monitor specific values
Basic Debugger Commands (pdb)
import pdb
def buggy_function(x):
pdb.set_trace() # Execution stops here!
result = x * 2
return result
# Common pdb commands:
# n (next): Execute next line
# s (step): Step into function calls
# c (continue): Run until next breakpoint
# p variable: Print variable value
# q (quit): Stop debugging
Common Bug Patterns
Pattern 1: Off-by-One Errors
# Bug: Accessing index that doesn't exist
items = [1, 2, 3]
for i in range(len(items) + 1): # Should be range(len(items))
print(items[i]) # Crashes on last iteration!
Pattern 2: Wrong Variable
# Bug: Using wrong variable
total = 0
for item in items:
total += items # Should be 'item', not 'items'!
Pattern 3: Forgetting to Return
# Bug: Function returns None
def double(x):
result = x * 2
# Forgot: return result
value = double(5)
print(value * 2) # TypeError: unsupported operand type
Pattern 4: Mutable Default Arguments
# Bug: Default list is shared between calls!
def add_item(item, items=[]): # BAD!
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] - Unexpected!
# Fix: Use None as default
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Preventing Bugs
The best bug is one that never happens!
# 1. Use meaningful variable names
# Bad
x = 5
# Good
user_age = 5
# 2. Write small, focused functions
# Bad: 100-line function doing everything
# Good: Multiple small functions, each doing one thing
# 3. Add type hints
def greet(name: str) -> str:
return f"Hello, {name}"
# 4. Validate inputs early
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# 5. Write tests!
def test_divide():
assert divide(10, 2) == 5
assert divide(0, 5) == 0
Debugging Checklist
When you're stuck, check these common issues:
Debugging Checklist
Is the error message telling me something useful?
Did I check the line number in the traceback?
Are my variable names spelled correctly?
Am I using the right variable?
Is the variable the type I expect? (print type())
Did I forget to return a value?
Am I off by one in my loop/index?
Did I save the file before running?
Am I running the right file?
Did I actually call the function?
Key Takeaways
Remember These Points
Follow the scientific method
Observe → Reproduce → Hypothesize → Test → Fix
Print debugging is simple and effective
print(f"DEBUG: variable = {variable}")
Rubber duck debugging really works!
Explain your code out loud
Learn your IDE's debugger
Breakpoints, stepping, variable inspection
Prevent bugs with good practices
Clear names, small functions, tests, validation
Module Complete!
Congratulations! You've mastered error handling in Python!
You can now:
- Understand and read error messages
- Catch and handle exceptions gracefully
- Raise your own exceptions when needed
- Debug problems systematically
In the next module, you'll learn about Modules and Packages – how to organize code and use Python's vast ecosystem of libraries!
