- What is debugging — finding and fixing bugs in your code
- Print debugging — the simplest way to debug
- pdb — the Python debugger
- Logging for debugging — using logs to find bugs
- Common bugs — and how to fix them
- Real-world debugging — practical techniques
What is Debugging?
Debugging is the process of finding and fixing bugs (errors) in your code. It's a skill every programmer needs to learn.
Think of debugging like detective work. You have a crime (the bug), you collect clues (error messages, logs), you interview witnesses (run the code), and you figure out what happened and why.
The good news is that debugging gets easier with practice. You'll learn to recognize common patterns and develop a systematic approach to finding bugs.
💡 Key concept: Debugging is a skill that takes practice. The more you do it, the better you get at finding and fixing bugs.
Print Debugging
The Simplest Debugging Technique
Print debugging is the most common debugging technique. You add print statements to your code to see what's happening at different points.
# Print Debugging
print("=" * 50)
print("PRINT DEBUGGING")
print("=" * 50)
# ============================================================
# WITHOUT PRINT DEBUGGING - Hard to find the bug
# ============================================================
print("\n1. WITHOUT PRINT DEBUGGING")
def calculate_average(numbers):
total = 0
for num in numbers:
total += num
return total / len(numbers)
# This function seems correct, but...
numbers = [10, 20, 30, 40, 50]
avg = calculate_average(numbers)
print(f" Average: {avg}") # Works fine
# But what if something is wrong?
numbers = []
try:
avg = calculate_average(numbers)
except ZeroDivisionError as e:
print(f" Error: {e}")
# We can't see what's happening inside the function
# ============================================================
# WITH PRINT DEBUGGING - Easy to see what's happening
# ============================================================
print("\n2. WITH PRINT DEBUGGING")
def calculate_average_with_print(numbers):
print(f" Inside calculate_average with numbers: {numbers}")
total = 0
print(f" Starting total: {total}")
for i, num in enumerate(numbers):
print(f" Iteration {i}: num = {num}")
total += num
print(f" New total: {total}")
print(f" Final total: {total}")
print(f" Length: {len(numbers)}")
if len(numbers) == 0:
print(" Empty list detected!")
return 0
result = total / len(numbers)
print(f" Average: {result}")
return result
print(" Testing with empty list:")
avg = calculate_average_with_print([])
print(f" Result: {avg}")
print("\n Testing with numbers:")
avg = calculate_average_with_print([10, 20, 30])
print(f" Result: {avg}")
# ============================================================
# PRINT DEBUGGING TIPS
# ============================================================
print("\n3. PRINT DEBUGGING TIPS")
print("""
┌─────────────────────────────────────────────────────────────────────┐
│ PRINT DEBUGGING TIPS │
├─────────────────────────────────────────────────────────────────────┤
│ 1. Print variable values at key points │
│ 2. Use descriptive messages: print(f"User ID: {user_id}") │
│ 3. Print before and after operations │
│ 4. Print the type of variables: print(type(variable)) │
│ 5. Print function arguments when entering a function │
│ 6. Add a marker: print("=== ENTERING LOOP ===") │
│ 7. Print loop iterations: print(f"i={i}, value={value}") │
│ 8. Remember to remove print statements after debugging │
└─────────────────────────────────────────────────────────────────────┘
""")
Print debugging key points:
- Add print statements — to see what's happening
- Print variable values — at key points
- Use markers — to identify different parts
- Remove later — clean up after debugging
Quick Check: What is the simplest debugging technique? (Answer: Print debugging — adding print statements)
Python Debugger (pdb)
Interactive Debugging with pdb
pdb (Python Debugger) is an interactive debugger that lets you step through your code line by line, inspect variables, and control the execution flow.
# Python Debugger (pdb)
print("=" * 50)
print("PYTHON DEBUGGER (pdb)")
print("=" * 50)
# ============================================================
# USING pdb - Set breakpoints
# ============================================================
print("\n1. USING pdb")
print("""
import pdb
def calculate_average(numbers):
pdb.set_trace() # Start debugger here
total = 0
for num in numbers:
total += num
return total / len(numbers)
# When you run this, the debugger will start at set_trace()
# and you can step through the code
""")
# ============================================================
# PDB COMMANDS
# ============================================================
print("\n2. PDB COMMANDS")
print("""
┌─────────────────────┬─────────────────────────────────────────────────────┐
│ Command │ What it does │
├─────────────────────┼─────────────────────────────────────────────────────┤
│ n (next) │ Execute the next line (step over) │
│ s (step) │ Step into a function call │
│ c (continue) │ Continue execution until next breakpoint │
│ p (print) │ Print a variable value: p variable │
│ pp (pretty print) │ Pretty print a variable │
│ l (list) │ Show source code around current line │
│ w (where) │ Show current stack trace │
│ u (up) │ Move up one frame in the stack │
│ d (down) │ Move down one frame in the stack │
│ q (quit) │ Quit the debugger │
│ h (help) │ Show help │
│ ! │ Execute a Python statement │
└─────────────────────┴─────────────────────────────────────────────────────┘
""")
# ============================================================
# BREAKPOINT() - Python 3.7+ (No import needed)
# ============================================================
print("\n3. breakpoint() - Python 3.7+")
print("""
def process_data(data):
total = 0
for item in data:
breakpoint() # Start debugger here
total += item
return total
# Just call breakpoint() where you want to start debugging
# No need to import pdb!
""")
# ============================================================
# EXAMPLE USAGE
# ============================================================
print("\n4. EXAMPLE USAGE")
print("""
def debug_example():
x = 10
y = 20
breakpoint() # Debugger starts here
z = x + y
return z
# When you run this:
# > (1)debug_example()
# -> z = x + y
# (Pdb) p x
# 10
# (Pdb) p y
# 20
# (Pdb) n
# > (1)debug_example()
# -> return z
# (Pdb) p z
# 30
# (Pdb) c
# 30
""")
# ============================================================
# RUNNING FROM COMMAND LINE
# ============================================================
print("\n5. RUNNING FROM COMMAND LINE")
print("""
# Run a script with the debugger from the start
python -m pdb my_script.py
# This starts pdb before the script runs
# You can set breakpoints and step through the code
# Set a breakpoint in the code
import pdb
pdb.set_trace() # Or breakpoint() in Python 3.7+
""")
pdb key points:
- pdb.set_trace() — start debugger
- breakpoint() — Python 3.7+ shortcut
- n (next) — step to next line
- p (print) — print variable values
- c (continue) — continue execution
Quick Check: What command prints a variable in pdb? (Answer: p variable_name)
Debugging with Logging
Using Logs for Debugging
Logging is a great way to debug, especially in production. It gives you a permanent record of what happened.
# Debugging with Logging
import logging
import time
print("=" * 50)
print("DEBUGGING WITH LOGGING")
print("=" * 50)
# ============================================================
# SETUP LOGGING FOR DEBUGGING
# ============================================================
print("\n1. SETUP LOGGING")
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# ============================================================
# FUNCTION WITH LOGGING
# ============================================================
print("\n2. FUNCTION WITH LOGGING")
def process_data_with_logging(data):
logger.debug(f"Processing data: {data}")
if not data:
logger.warning("Empty data received")
return []
result = []
for i, item in enumerate(data):
logger.debug(f"Processing item {i}: {item}")
try:
processed = item * 2
result.append(processed)
logger.debug(f"Item {i} processed: {processed}")
except Exception as e:
logger.error(f"Error processing item {i}: {e}", exc_info=True)
logger.info(f"Processed {len(result)} items out of {len(data)}")
return result
# Test with good data
print("\n Testing with good data:")
result = process_data_with_logging([1, 2, 3, 4, 5])
print(f" Result: {result}")
# Test with bad data
print("\n Testing with bad data:")
result = process_data_with_logging([1, "two", 3, None, 5])
print(f" Result: {result}")
# ============================================================
# LOG LEVELS FOR DEBUGGING
# ============================================================
print("\n3. LOG LEVELS FOR DEBUGGING")
print("""
┌─────────────┬─────────────────────────────────────────────────────┐
│ Level │ When to use for debugging │
├─────────────┼─────────────────────────────────────────────────────┤
│ DEBUG │ Detailed information for debugging │
│ INFO │ Confirmation that things are working │
│ WARNING │ Something unexpected but not an error │
│ ERROR │ A problem occurred │
│ CRITICAL │ A serious error │
└─────────────┴─────────────────────────────────────────────────────┘
Set level to DEBUG to see everything:
logging.basicConfig(level=logging.DEBUG)
In production, set to WARNING or ERROR:
logging.basicConfig(level=logging.WARNING)
""")
# ============================================================
# LOGGING VS PRINT
# ============================================================
print("\n4. LOGGING VS PRINT")
print("""
Logging advantages over print:
✅ Timestamps automatically added
✅ Different log levels (DEBUG, INFO, WARNING, ERROR)
✅ Can be disabled without removing code
✅ Can write to files
✅ Can be configured for different environments
✅ Thread-safe
✅ Includes context (module, function, line number)
When to use:
• Print: Quick local debugging
• Logging: Production debugging, permanent records
""")
Logging for debugging key points:
- Use DEBUG level — for detailed debugging info
- Log function entries — know when functions are called
- Log variable values — see what's being processed
- Log errors — with traceback for exceptions
- Better than print — timestamps, levels, files
Quick Check: What log level should you use for debugging? (Answer: DEBUG)
Common Bugs and How to Fix Them
Fixing Common Python Bugs
Here are some of the most common bugs in Python and how to fix them.
# Common Bugs and How to Fix Them
print("=" * 50)
print("COMMON BUGS AND HOW TO FIX THEM")
print("=" * 50)
# ============================================================
# BUG 1: IndexError - List index out of range
# ============================================================
print("\n1. IndexError - List index out of range")
# Buggy code
my_list = [1, 2, 3]
try:
print(f" my_list[3]: {my_list[3]}") # IndexError
except IndexError as e:
print(f" Error: {e}")
# Fix: Check length before accessing
def safe_get(lst, index, default=None):
if 0 <= index < len(lst):
return lst[index]
return default
print(f" safe_get(my_list, 3): {safe_get(my_list, 3)}")
# ============================================================
# BUG 2: KeyError - Dictionary key not found
# ============================================================
print("\n2. KeyError - Dictionary key not found")
# Buggy code
my_dict = {"name": "Alice"}
try:
print(f" my_dict['age']: {my_dict['age']}") # KeyError
except KeyError as e:
print(f" Error: {e}")
# Fix: Use get() method
print(f" my_dict.get('age', 'Unknown'): {my_dict.get('age', 'Unknown')}")
# ============================================================
# BUG 3: TypeError - Operation with wrong type
# ============================================================
print("\n3. TypeError - Operation with wrong type")
# Buggy code
try:
result = "5" + 3 # TypeError
except TypeError as e:
print(f" Error: {e}")
# Fix: Convert types properly
print(f" int('5') + 3: {int('5') + 3}")
print(f" '5' + str(3): {'5' + str(3)}")
# ============================================================
# BUG 4: ValueError - Invalid value for operation
# ============================================================
print("\n4. ValueError - Invalid value")
# Buggy code
try:
num = int("abc") # ValueError
except ValueError as e:
print(f" Error: {e}")
# Fix: Validate input
def safe_int(value):
try:
return int(value)
except ValueError:
return None
print(f" safe_int('abc'): {safe_int('abc')}")
print(f" safe_int('123'): {safe_int('123')}")
# ============================================================
# BUG 5: AttributeError - Object has no attribute
# ============================================================
print("\n5. AttributeError - Object has no attribute")
# Buggy code
class Person:
def __init__(self, name):
self.name = name
p = Person("Alice")
try:
print(f" p.age: {p.age}") # AttributeError
except AttributeError as e:
print(f" Error: {e}")
# Fix: Check if attribute exists
print(f" hasattr(p, 'age'): {hasattr(p, 'age')}")
print(f" getattr(p, 'age', 'Unknown'): {getattr(p, 'age', 'Unknown')}")
# ============================================================
# BUG 6: ZeroDivisionError - Division by zero
# ============================================================
print("\n6. ZeroDivisionError - Division by zero")
# Buggy code
try:
result = 10 / 0 # ZeroDivisionError
except ZeroDivisionError as e:
print(f" Error: {e}")
# Fix: Check before dividing
def safe_divide(a, b):
if b == 0:
return None
return a / b
print(f" safe_divide(10, 0): {safe_divide(10, 0)}")
# ============================================================
# BUG 7: IndentationError - Wrong indentation
# ============================================================
print("\n7. IndentationError - Wrong indentation")
print("""
# Wrong indentation
def buggy_function():
print("This is wrong") # IndentationError
# Correct indentation
def good_function():
print("This is correct")
""")
# ============================================================
# BUG 8: NameError - Variable not defined
# ============================================================
print("\n8. NameError - Variable not defined")
# Buggy code
try:
print(f" undefined_variable: {undefined_variable}") # NameError
except NameError as e:
print(f" Error: {e}")
# Fix: Define the variable
defined_variable = "I'm defined"
print(f" defined_variable: {defined_variable}")
Common bugs key points:
- IndexError — check list bounds
- KeyError — use
dict.get() - TypeError — convert types properly
- ValueError — validate input
- AttributeError — check with
hasattr()
Quick Check: How do you safely access a dictionary key? (Answer: Use dict.get(key, default))
Real-World Debugging
Debugging a Real Application
# Real-World Debugging Example
print("=" * 60)
print("REAL-WORLD DEBUGGING")
print("=" * 60)
# ============================================================
# BUGGY APPLICATION - A simple user manager
# ============================================================
import logging
import pdb
# Set up logging for debugging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class UserManager:
"""Manage users with a bug"""
def __init__(self):
self.users = []
self.user_counter = 0
def add_user(self, name, email):
"""Add a user - has a bug!"""
# BUG: Missing check for email format
# BUG: Missing check for duplicate user
self.user_counter += 1
user = {
"id": self.user_counter,
"name": name,
"email": email,
"active": True
}
self.users.append(user)
logger.debug(f"Added user: {user}")
return user
def get_user(self, user_id):
"""Get a user by ID - has a bug!"""
# BUG: Returns None if user not found
# Should raise an exception or handle properly
for user in self.users:
if user["id"] == user_id:
return user
return None
def delete_user(self, user_id):
"""Delete a user - has a bug!"""
# BUG: Removes user but doesn't handle case where user not found
# BUG: Doesn't check if user is active before deleting
for i, user in enumerate(self.users):
if user["id"] == user_id:
self.users.pop(i)
logger.debug(f"Deleted user {user_id}")
return True
return False
def get_active_users(self):
"""Get all active users - has a bug!"""
# BUG: Returns None if no active users, should return empty list
active = [user for user in self.users if user["active"]]
if not active:
return None # BUG: Should return []
return active
# ============================================================
# DEBUGGING PROCESS - Step by Step
# ============================================================
print("\n1. IDENTIFYING THE BUG")
print("""
Problem: The application crashes when trying to get active users
when there are no active users.
Let's add print statements to understand what's happening.
""")
# Create the manager
manager = UserManager()
# Add some users
manager.add_user("Alice", "alice@example.com")
manager.add_user("Bob", "bob@example.com")
print(" Initial users:", manager.users)
# Test get_active_users - should return all users
print("\n2. TEST GET_ACTIVE_USERS")
print(" Before fix - get_active_users():")
result = manager.get_active_users()
print(f" Result: {result}") # Returns a list of users
# Test when no users - this is where the bug appears
print("\n3. TEST WITH NO USERS")
# Create a new manager with no users
empty_manager = UserManager()
print(" New manager with no users")
print(" Before fix - get_active_users():")
try:
result = empty_manager.get_active_users()
print(f" Result: {result}") # This returns None
print(f" Type: {type(result)}")
except Exception as e:
print(f" Error: {e}")
print("\n The bug: get_active_users returns None instead of []")
# ============================================================
# FIXING THE BUG
# ============================================================
print("\n4. FIXING THE BUG")
# Fixed version
class FixedUserManager(UserManager):
"""Fixed version of UserManager"""
def get_active_users(self):
"""Get all active users - fixed version"""
active = [user for user in self.users if user["active"]]
return active # Always return a list, even if empty
# Test the fix
print(" Testing the fix:")
fixed_manager = FixedUserManager()
result = fixed_manager.get_active_users()
print(f" get_active_users() on empty manager: {result}")
print(f" Type: {type(result)}")
print(f" Bug fixed! Returns [] instead of None")
# ============================================================
# DEBUGGING PROCESS SUMMARY
# ============================================================
print("\n5. DEBUGGING PROCESS SUMMARY")
print("""
1. Reproduce the bug - know how to make it happen
2. Add print/logging statements - see what's happening
3. Use the debugger (pdb) - step through the code
4. Find the root cause - understand why it's happening
5. Fix the bug - make the change
6. Test the fix - verify it works
7. Look for other bugs - test other cases
8. Remove debugging code - clean up
""")
Real-world debugging key points:
- Reproduce the bug — know how to trigger it
- Add logging — understand what's happening
- Use the debugger — step through the code
- Find the root cause — why is it happening?
- Fix and test — verify the fix works
Quick Check: What's the first step in debugging? (Answer: Reproduce the bug)
Best Practices
Debugging Best Practices
# Debugging Best Practices
print("=" * 60)
print("DEBUGGING BEST PRACTICES")
print("=" * 60)
# ============================================================
# 1. READ THE ERROR MESSAGE
# ============================================================
print("\n1. READ THE ERROR MESSAGE")
print("""
# Error messages tell you:
# - What went wrong
# - Where it went wrong (line number)
# - What type of error it is
Example:
Traceback (most recent call last):
File "my_script.py", line 10, in
result = calculate(data)
File "my_script.py", line 5, in calculate
return total / len(numbers)
ZeroDivisionError: division by zero
This tells us:
• Error type: ZeroDivisionError
• Location: line 5 in calculate function
• Cause: division by zero
""")
# ============================================================
# 2. USE THE DEBUGGER
# ============================================================
print("\n2. USE THE DEBUGGER")
print("""
# Don't just use print statements
# Use the debugger to step through code
# Insert a breakpoint
breakpoint() # Python 3.7+
# or
import pdb
pdb.set_trace()
# Then use commands:
# n - next line
# p variable - print variable
# c - continue
""")
# ============================================================
# 3. WRITE TESTS
# ============================================================
print("\n3. WRITE TESTS")
print("""
# Tests help prevent bugs and catch them early
def test_calculate_average():
assert calculate_average([1, 2, 3]) == 2
assert calculate_average([]) == 0 # This will fail if bug exists
# Run tests regularly
# pytest test_my_code.py
""")
# ============================================================
# 4. USE LOGGING INSTEAD OF PRINT
# ============================================================
print("\n4. USE LOGGING INSTEAD OF PRINT")
print("""
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# In production, turn off DEBUG logs
# In development, keep them on
logger.debug("Debug message - for development")
logger.info("Info message - for production")
logger.error("Error message - always show")
""")
# ============================================================
# 5. TAKE BREAKS
# ============================================================
print("\n5. TAKE BREAKS")
print("""
Sometimes the best debugging technique is to step away.
When you're stuck:
- Walk away for 5-10 minutes
- Come back with fresh eyes
- Explain the problem to someone else (rubber duck debugging)
The solution often becomes obvious when you look at it again.
""")
# ============================================================
# 6. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("DEBUGGING BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
- Read the error message carefully
- Use the debugger (pdb)
- Write tests to catch bugs early
- Use logging instead of print
- Take breaks when stuck
- Reproduce the bug reliably
- Check your assumptions
- Use version control to track changes
- Look for the simplest explanation
- Document the fix
""")
Best practices summary:
- Read error messages — they tell you what's wrong
- Use the debugger — pdb is powerful
- Write tests — prevent and catch bugs
- Use logging — better than print
- Take breaks — fresh eyes help
Quick Check: What should you do when you're stuck debugging? (Answer: Take a break, come back with fresh eyes)
Try It Yourself
Practice debugging in the editor below.
DEBUGGING - PRACTICE
==================================================
1. BUGGY CODE
Normal case: 30.0
Error: division by zero
Single element: 42.0
2. FIXED CODE
Normal case: 30.0
Empty list: 0
Single element: 42.0
You've Got It!
You now understand debugging in Python. You know how to use print debugging, the Python debugger (pdb), logging, and common debugging techniques.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is debugging in Python?
What's the difference between print debugging and using pdb?
How do I use breakpoint() in Python?
breakpoint() to start the debugger. When the code hits this line, it pauses and enters the debugger. You can then use pdb commands to inspect and step through the code.
What are the most common Python errors?
Should I use print or logging for debugging?
How can I prevent bugs?
Where to Go From Here
Now that you understand debugging, check out these related topics:
Logging
Learn more about logging for debugging.
Learn More →Exception Handling
Learn how to handle exceptions properly.
Learn More →Testing Assignments
Practice your debugging and testing skills.
Learn More →