19 Debugging - Finding and Fixing Code Mysteries
19.1 Chapter Outline
- Understanding debugging fundamentals
- Common debugging techniques
- Using print statements effectively
- Working with Python’s debugger (pdb)
- Recognizing common bug patterns
- Debugging strategies for different error types
- Preventing bugs through better coding practices
19.2 Learning Objectives
By the end of this chapter, you will be able to: - Understand the debugging mindset and process - Use print statements to inspect your program’s state - Apply systematic debugging techniques to find errors - Recognize and fix common bug patterns - Use Python’s built-in debugging tools - Apply debugging strategies for different types of errors - Develop habits that prevent bugs in your code
19.3 1. Introduction: The Art of Debugging
Every programmer, from beginner to expert, writes code with bugs. Debugging is the process of finding and fixing these errors, and it’s a crucial skill that often separates novice programmers from experienced ones. As software pioneer Brian Kernighan said:
“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.”
In the previous chapter, we looked at how to handle runtime errors (exceptions) that Python detects and reports. In this chapter, we’ll focus on a more challenging type of error: logical errors where the code runs without crashing but doesn’t produce the expected results.
AI Tip: When you’re stuck on a bug, explain your code line by line to your AI assistant. The process of explaining often helps you spot the issue yourself, a technique known as “rubber duck debugging.”
19.4 2. Understanding Debugging Fundamentals
19.4.1 Types of Errors Revisited
As a reminder, there are three main types of errors in programming:
- Syntax Errors: Code doesn’t follow language rules (Python catches these automatically)
- Runtime Errors/Exceptions: Code runs but fails during execution (covered in Chapter 16)
- Logical Errors/Bugs: Code runs without errors but produces incorrect results
Debugging primarily focuses on the third type, which is the most challenging. These errors don’t trigger exceptions but produce unexpected or incorrect behaviors.
19.4.2 The Debugging Mindset
Effective debugging requires a particular mindset:
- Be systematic: Follow a methodical approach rather than making random changes
- Be curious: Ask “why” repeatedly to get to the root cause
- Be patient: Some bugs take time to find and fix
- Be scientific: Form hypotheses, test them, and analyze results
- Be persistent: Don’t give up when the solution isn’t immediately obvious
19.4.3 The Debugging Process
A systematic debugging process typically follows these steps:
- Reproduce the bug: Find reliable steps to make the problem occur
- Isolate the problem: Narrow down where the bug might be
- Inspect the state: Examine variables and program flow
- Form a hypothesis: Make an educated guess about the cause
- Test the fix: Apply a solution and verify it works
- Review the code: Look for similar issues elsewhere in your code
19.5 3. The Print Statement: Your First Debugging Tool
The simplest and often most effective debugging technique is using print statements to see what’s happening in your code:
def calculate_average(numbers):
print(f"Input to calculate_average: {numbers}")
= sum(numbers)
total print(f"Sum of numbers: {total}")
= total / len(numbers)
average print(f"Calculated average: {average}")
return average
# Bug: This will return the wrong average
= [85, 90, 78]
scores = calculate_average(scores)
avg print(f"Average score: {avg}")
Strategic print statements can reveal: - Input values (what data is the function receiving?) - Intermediate values (what calculations are happening?) - Output values (what is being returned?)
19.5.1 Enhancing Print Statements
Make your print statements more useful by:
# Include context in your print messages
print(f"DEBUG - calculate_average() - received input: {numbers}")
# Use visual separators for important information
print("="*50)
print("CRITICAL VALUE:", result)
print("="*50)
# Print variable types when values look correct but operations fail
print(f"Value: {value}, Type: {type(value)}")
19.5.2 Temporary Debugging Code
Remember to remove or comment out debugging print statements when you’re done:
def calculate_average(numbers):
# DEBUG print(f"Input: {numbers}")
= sum(numbers)
total # DEBUG print(f"Sum: {total}")
= total / len(numbers)
average return average
Adding # DEBUG
makes it easier to find and remove these statements later.
19.6 4. Debugging with Python’s Built-in Tools
19.6.1 The pdb
Module
Python includes a built-in debugger called pdb
(Python DeBugger) that lets you pause code execution and inspect variables:
import pdb
def calculate_average(numbers):
= sum(numbers)
total # Code execution pauses here
pdb.set_trace() = total / len(numbers)
average return average
= [85, 90, 78]
scores = calculate_average(scores) avg
When the set_trace()
function runs, the program pauses and gives you a special prompt where you can: - Inspect variable values - Execute Python statements - Step through the code line by line - Continue execution
19.6.2 Common pdb Commands
In the debugger prompt, you can use: - p variable_name
- Print a variable’s value - n
- Execute the next line (step over) - s
- Step into a function call - c
- Continue execution until the next breakpoint - q
- Quit the debugger - h
- Help on debugger commands
19.6.3 Using Breakpoints in Python 3.7+
In newer Python versions, you can use a simpler breakpoint function:
def calculate_average(numbers):
= sum(numbers)
total breakpoint() # Equivalent to pdb.set_trace()
= total / len(numbers)
average return average
19.7 5. Common Bug Patterns and How to Find Them
19.7.1 Off-by-One Errors
These occur when your loop iterates one too many or too few times:
# Bug: This only processes the first n-1 items
def process_items(items):
for i in range(len(items) - 1): # Should be range(len(items))
process_item(items[i])
Debugging Tip: Print loop indices and boundary values to check iteration ranges.
19.7.2 Type Mismatches
These bugs happen when a value’s type is different from what you expect:
# Bug: user_age from input() is a string, not an integer
= input("Enter your age: ")
user_age = 65 - user_age # TypeError: can't subtract string years_until_retirement
Debugging Tip: Print both the value and type of suspicious variables, e.g., print(f"user_age: {user_age}, type: {type(user_age)}")
.
19.7.3 Logic Errors
Errors in the code’s logic that give incorrect results:
# Bug: Logic error in calculating average
= [85, 90, 78]
scores = sum(scores) / 4 # Should divide by len(scores), which is 3 average
Debugging Tip: Break complex expressions into smaller parts and print each part.
19.7.4 Missing Initialization
Failing to initialize a variable before using it:
# Bug: total is not initialized before the loop
# total = 0 # This line is missing
for num in numbers:
+= num # NameError: name 'total' is not defined total
Debugging Tip: Use print statements at the beginning of functions to verify variable initialization.
19.7.5 Scope Issues
Using variables from the wrong scope:
def calculate_total(items):
# Bug: Trying to access a global variable that doesn't exist
# or using a variable before defining it
return items_count * average_price # NameError
Debugging Tip: Print all variables used in a calculation to verify they exist in the current scope.
19.8 6. Debugging Strategies for Different Error Types
19.8.1 Strategy for Logical Errors
When your code runs but gives incorrect results:
- Add print statements at key points to track variable values
- Compare expected vs. actual values at each step
- Check boundary conditions (first iteration, last iteration, empty collections)
- Break down complex expressions into simpler parts
- Test with simple inputs where you can calculate the correct result by hand
19.8.2 Strategy for Intermittent Bugs
When bugs only appear sometimes:
- Look for race conditions or timing issues
- Check for random inputs or behaviors
- Search for hidden dependencies on external factors
- Add extensive logging to capture the state when the bug occurs
- Try to make the bug reproducible with specific inputs
19.8.3 Strategy for “It Worked Yesterday” Bugs
When code that used to work suddenly breaks:
- Review recent changes to the code
- Check for changes in dependencies or external resources
- Verify input data hasn’t changed
- Roll back changes one by one to find the breaking change
- Look for environmental differences between systems
19.9 7. Debugging in Practice: A Real Example
Let’s debug a function with a problem:
def find_highest_scorer(student_scores):
= 0
highest_score = None
highest_scorer
for student, score in student_scores.items():
if score > highest_score:
= score
highest_score = student
highest_scorer
return highest_scorer
# Test case
= {"Alice": 85, "Bob": 92, "Charlie": 78, "Diana": -5}
scores = find_highest_scorer(scores)
top_student print(f"The highest scorer is {top_student}") # Should be "Bob"
If we add a student with a negative score, we need to handle that case:
# Bug: If all scores are negative, this function fails
= {"Alice": -10, "Bob": -5, "Charlie": -20}
scores = find_highest_scorer(scores)
top_student print(f"The highest scorer is {top_student}") # Should be "Bob" but returns None
19.9.1 Debugging the Example
Let’s add print statements to investigate:
def find_highest_scorer(student_scores):
print(f"Scores received: {student_scores}")
= 0 # Bug is here - this should be initialized differently
highest_score = None
highest_scorer
for student, score in student_scores.items():
print(f"Checking {student} with score {score}")
print(f"Current highest: {highest_score} by {highest_scorer}")
if score > highest_score:
print(f"New highest score found: {score}")
= score
highest_score = student
highest_scorer
print(f"Final highest scorer: {highest_scorer} with {highest_score}")
return highest_scorer
The output reveals our bug:
Scores received: {'Alice': -10, 'Bob': -5, 'Charlie': -20}
Checking Alice with score -10
Current highest: 0 by None
Checking Bob with score -5
Current highest: 0 by None
Checking Charlie with score -20
Current highest: 0 by None
Final highest scorer: None with 0
The issue is that we initialized highest_score
to 0, but all scores are negative, so none pass the score > highest_score
check. Here’s the fix:
def find_highest_scorer(student_scores):
if not student_scores:
return None
# Initialize with the first student's score
= list(student_scores.keys())
students = students[0]
highest_scorer = student_scores[highest_scorer]
highest_score
for student, score in student_scores.items():
if score > highest_score:
= score
highest_score = student
highest_scorer
return highest_scorer
19.10 8. Self-Assessment Quiz
- What is the primary difference between debugging and exception handling?
- Debugging is for syntax errors; exception handling is for runtime errors
- Debugging is for finding errors; exception handling is for responding to known errors
- Debugging is a development activity; exception handling is a runtime activity
- All of the above
- Which of these is NOT a common debugging technique?
- Adding print statements
- Using a debugger like pdb
- Adding try/except blocks
- Rubber duck debugging (explaining code to an inanimate object)
- In the Python debugger (pdb), which command continues execution until the next breakpoint?
n
s
c
r
- What is an “off-by-one” error?
- A mathematical error where calculations are off by one unit
- A loop iteration error where the loop runs one too many or too few times
- An indexing error where you access the wrong element in a sequence
- All of the above
- What’s the best first step when encountering a bug in your code?
- Immediately start changing code to try to fix it
- Reproduce the bug with a simple, reliable test case
- Add print statements everywhere
- Ask someone else to fix it
Answers & Feedback: 1. d) All of the above — Debugging and exception handling serve different purposes and occur at different times 2. c) Adding try/except blocks — This is error handling, not debugging 3. c) c
— This continues execution until a breakpoint or the program ends 4. d) All of the above — Off-by-one errors can manifest in various ways 5. b) Reproduce the bug with a simple, reliable test case — Always start by making sure you can reliably recreate the issue
19.11 9. Debugging Tools Beyond Print Statements
19.11.1 Logging
For more sophisticated debugging, use Python’s logging
module:
import logging
# Configure logging
logging.basicConfig(=logging.DEBUG,
levelformat='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
='debug.log'
filename
)
def calculate_average(numbers):
f"Calculate_average called with {numbers}")
logging.debug(if not numbers:
"Empty list provided, returning 0")
logging.warning(return 0
= sum(numbers)
total f"Sum calculated: {total}")
logging.debug(= total / len(numbers)
average f"Average calculated: {average}")
logging.debug(return average
Advantages of logging over print statements: - Log to a file instead of the console - Use different severity levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) - Include timestamps and other metadata - Can be enabled/disabled without removing code
19.11.2 Assertions
Use assertions to verify assumptions in your code:
def calculate_average(numbers):
assert len(numbers) > 0, "Cannot calculate average of empty list"
= sum(numbers)
total = total / len(numbers)
average return average
If the condition after assert
is False, Python raises an AssertionError
with the provided message.
19.12 Project Corner: Debugging Your Chatbot
Let’s apply debugging techniques to enhance our chatbot’s reliability:
import random
import logging
import datetime
import os
# Configure logging
logging.basicConfig(=logging.DEBUG,
levelformat='%(asctime)s - %(levelname)s - %(message)s',
='chatbot_debug.log'
filename
)
# Response patterns
= {
response_patterns "greetings": ["hello", "hi", "hey", "howdy", "hola"],
"farewells": ["bye", "goodbye", "see you", "cya", "farewell"],
"gratitude": ["thanks", "thank you", "appreciate"],
"bot_questions": ["who are you", "what are you", "your name"],
"user_questions": ["how are you", "what's up", "how do you feel"]
}
= {
response_templates "greetings": ["Hello there!", "Hi! Nice to chat with you!"],
"farewells": ["Goodbye! Come back soon!", "See you later!"],
"gratitude": ["You're welcome!", "Happy to help!"],
"bot_questions": ["I'm PyBot, a simple chatbot built with Python!"],
"user_questions": ["I'm functioning well, thanks for asking!"]
}
class DebugChatbot:
"""A chatbot with enhanced debugging capabilities."""
def __init__(self, name="PyBot"):
self.name = name
self.user_name = None
self.conversation_history = []
self.response_patterns = response_patterns
self.response_templates = response_templates
self.debug_mode = False
f"Chatbot {name} initialized")
logging.info(
def toggle_debug(self):
"""Toggle debug mode on/off."""
self.debug_mode = not self.debug_mode
= "ON" if self.debug_mode else "OFF"
status f"Debug mode turned {status}")
logging.info(return f"Debug mode is now {status}"
def debug_print(self, message):
"""Print debug messages if debug mode is on."""
if self.debug_mode:
print(f"DEBUG: {message}")
logging.debug(message)
def get_response(self, user_input):
"""Generate a response with debugging information."""
self.debug_print(f"Processing input: '{user_input}'")
if not user_input:
self.debug_print("Empty input received")
return "I didn't catch that. Could you please say something?"
= user_input.lower()
user_input self.debug_print(f"Lowercase input: '{user_input}'")
# Check if this is a debug command
if user_input == "debug":
return self.toggle_debug()
# Check each category of responses
for category, patterns in self.response_patterns.items():
self.debug_print(f"Checking category: {category}")
for pattern in patterns:
if pattern in user_input:
self.debug_print(f"Pattern match found: '{pattern}'")
# Get response templates for this category
= self.response_templates.get(category)
templates self.debug_print(f"Found {len(templates)} possible responses")
# Select a random response
= random.choice(templates)
response self.debug_print(f"Selected response: '{response}'")
return response
# No pattern matched
self.debug_print("No pattern matches found")
return "I'm still learning. Can you tell me more?"
def run(self):
"""Run the chatbot with error tracing."""
try:
print(f"Hello! I'm {self.name}. Type 'bye' to exit or 'debug' to toggle debug mode.")
self.user_name = input("What's your name? ")
f"User identified as {self.user_name}")
logging.info(print(f"Nice to meet you, {self.user_name}!")
self.add_to_history(self.name, f"Nice to meet you, {self.user_name}!")
while True:
try:
= input(f"{self.user_name}> ")
user_input self.add_to_history(self.user_name, user_input)
if user_input.lower() in ["bye", "goodbye", "exit"]:
= f"Goodbye, {self.user_name}!"
response print(f"{self.name}> {response}")
self.add_to_history(self.name, response)
break
= self.get_response(user_input)
response print(f"{self.name}> {response}")
self.add_to_history(self.name, response)
except Exception as e:
= f"Error in conversation loop: {str(e)}"
error_msg =True)
logging.error(error_msg, exc_infoif self.debug_mode:
print(f"DEBUG ERROR: {error_msg}")
print(f"{self.name}> Sorry, I encountered a problem. Let's continue.")
except Exception as e:
f"Critical error in chatbot: {str(e)}", exc_info=True)
logging.critical(print(f"Critical error: {str(e)}")
print("Check the log file for details.")
def add_to_history(self, speaker, text):
"""Add a message to conversation history with timestamp."""
= datetime.datetime.now().strftime("%H:%M:%S")
timestamp = {
entry "speaker": speaker,
"text": text,
"timestamp": timestamp
}self.conversation_history.append(entry)
self.debug_print(f"Added to history: {entry}")
# Create and run the chatbot
if __name__ == "__main__":
= DebugChatbot()
chatbot chatbot.run()
This enhanced chatbot includes:
- Logging: Records detailed information for later analysis
- Debug Mode: Toggleable detailed output with the “debug” command
- Error Handling: Catches and logs exceptions without crashing
- Detailed Tracing: Tracks the processing of each user input
- Structured History: Stores conversations with timestamps
Debugging Challenges: - Add assertions to verify the integrity of the conversation history - Implement a “replay” command that shows the exact steps of how a response was generated - Create a “why” command that explains why the bot gave a particular response - Add more detailed logging for file operations - Create a visual representation of the chatbot’s decision tree
19.13 Cross-References
- Previous Chapter: Errors and Exceptions
- Next Chapter: Testing
- Related Topics: Errors and Exceptions (Chapter 16), Functions (Chapter 9)
AI Tip: When debugging, describe your expectations, what actually happened, and the code you’re working with to your AI assistant. It can often spot patterns and suggest debugging approaches you might not have considered.
19.14 Preventing Bugs: The Best Debugging is No Debugging
While debugging skills are essential, preventing bugs in the first place is even better:
19.14.1 Write Clear, Simple Code
The more complex your code, the more places for bugs to hide:
# Hard to debug
= sum([x for x in data if x > threshold]) / len([y for y in data if y > 0])
result
# Easier to debug - break it down
= [x for x in data if x > threshold]
valid_values = sum(valid_values)
total = len([y for y in data if y > 0])
positive_count = total / positive_count result
19.14.2 Document Your Assumptions
Make implicit assumptions explicit through comments and assertions:
def calculate_average(numbers):
"""Calculate the average of a list of numbers.
Args:
numbers: A non-empty list of numeric values
Returns:
The arithmetic mean of the numbers
Raises:
ZeroDivisionError: If the input list is empty
"""
# Assumption: numbers is a non-empty list
assert len(numbers) > 0, "numbers list cannot be empty"
return sum(numbers) / len(numbers)
19.14.3 Write Tests
Testing (covered in the next chapter) helps you catch bugs early:
def test_calculate_average():
assert calculate_average([1, 2, 3]) == 2
assert calculate_average([0, 0, 0]) == 0
assert calculate_average([-1, 1]) == 0
# Test edge cases too
assert calculate_average([1000000]) == 1000000
19.14.4 Use Consistent Conventions
Consistent code style reduces confusion and errors:
# Consistent naming makes code more predictable
# Variables in snake_case
= "Alice"
user_name = 100
total_amount
# Constants in UPPERCASE
= 3
MAX_ATTEMPTS = 30
DEFAULT_TIMEOUT
# Functions in snake_case
def calculate_total(items):
pass
# Classes in CamelCase
class UserAccount:
pass
By combining effective debugging techniques with preventative practices, you’ll find and fix bugs faster—and create fewer of them in the first place. Remember that debugging is a skill that improves with practice, so don’t get discouraged when you encounter challenging bugs. Each one you solve makes you a better programmer.