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.

# 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

prices = [10, 20, 30]
tax_rate = 0.08

result = calculate_total(prices, tax_rate)
print(f"Total: {result}")
Expected Output:
DEBUG: prices = [10, 20, 30]
DEBUG: subtotal = 60
DEBUG: tax = 4.8
DEBUG: total = 64.8
Total: 64.8

Print 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)}")
print(f"discount = {discount}, type = {type(discount)}")

# The types explain the failure: you cannot subtract an int from a str
try:
    calculate_discount(price, discount)
except TypeError as e:
    print(f"TypeError: {e}")

# Fix: Convert to proper type
price = int(price)
result = calculate_discount(price, discount)
print(f"Result: {result}")

# 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)
debug_print("discount", discount)
Expected Output:
price = 100, type = <class 'str'>
discount = 10, type = <class 'int'>
TypeError: unsupported operand type(s) for -: 'str' and 'int'
Result: 90
price = 100 (type: int)
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")
print(f"Result 1: {result1}\n")

result2 = process_user_age("abc")
print(f"Result 2: {result2}\n")

# Note "-5" never reaches the age < 0 branch: isdigit() is False for a
# string with a minus sign, so it is rejected as "not a digit" first
result3 = process_user_age("-5")
print(f"Result 3: {result3}")
Expected Output:
[ENTRY] process_user_age called with: '25'
[CONVERTED] age_str to int: 25
[BRANCH] Age is valid
[EXIT] Returning: 25
Result 1: 25

[ENTRY] process_user_age called with: 'abc'
[BRANCH] Invalid input - not a digit
Result 2: None

[ENTRY] process_user_age called with: '-5'
[BRANCH] Invalid input - not a digit
Result 3: None

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)

# Pretty print (much better). Note that pprint sorts dictionary keys, so
# 'address' comes first even though 'id' was inserted first
print("\nPretty print (note that pprint sorts the keys):")
pprint(users)

# A narrower width forces more line breaks
print("\nFirst user, narrower:")
pprint(users[0], width=40)
Expected Output:
Regular print:
[{'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]}]

Pretty print (note that pprint sorts the keys):
[{'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]}]

First user, narrower:
{'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])

# 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)
Expected 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
==================================================
🐛 calculate() called
🐛 x: 10
🐛 y: 20
🐛 result: 30

Remember 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.

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

result = process_scores([])
Expected Output:
Traceback (most recent call last):
  File "script.py", line 11, in <module>
    result = process_scores([])
  File "script.py", line 8, in process_scores
    avg = calculate_average(scores)
  File "script.py", line 4, in calculate_average
    average = total / count
              ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

How to read that, working from the bottom up:

  • The last line is the answer. ZeroDivisionError is what went wrong, and "division by zero" is why.
  • The frame above it is where. Line 4,average = total / count. The~~~~~~^~~~~~~ underneath points at the exact sub-expression that failed, which matters on a line with several operations.
  • The frames above that are how you got there. Reading upward: calculate_average was called byprocess_scores on line 8, which was called from the top level on line 11. The empty list came in at the top and was passed straight down.

The fix belongs where the assumption is made, not where it blows up: the division is only unsafe because nothing checked the list for being empty.

# Fix: Add validation
def calculate_average(numbers):
    if len(numbers) == 0:
        return 0  # or raise ValueError("Cannot average an empty list")

    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return average

def process_scores(scores):
    avg = calculate_average(scores)
    return avg * 1.1

result = process_scores([])
print(result)
Expected Output:
0.0

Common Error Types

# The failing line of each pair is commented out so this file runs top to
# bottom. Uncomment ONE at a time to see the error for yourself.

# 1. SyntaxError - Code structure is wrong
# print("Hello"  # Missing closing parenthesis
# SyntaxError: '(' was never closed

# 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
print(result)
# or
result = "5" + str(10)  # Convert int to string
print(result)

# 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)

# 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 block after function definition on line 1
Expected Output:
15
510
Unknown

Reading 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()
Expected Output:
Level 1 started
Level 2 started
Level 3 started
Traceback (most recent call last):
  File "script.py", line 19, in <module>
    level_1()
    ~~~~~~~^^
  File "script.py", line 4, in level_1
    level_2()
    ~~~~~~~^^
  File "script.py", line 9, in level_2
    level_3()
    ~~~~~~~^^
  File "script.py", line 15, in level_3
    print(problem[0])  # This will error!
          ~~~~~~~^^^
IndexError: list index out of range

The three "Level N started" lines print before the traceback, which tells you the code got all the way down to level_3 before failing. Then the frames read upward: the error happened on line 15 inlevel_3, which line 9 oflevel_2 called, which line 4 oflevel_1 called, which the top level called on line 19. Notice that no "finished" line ever prints: the exception unwound every frame on its way out.

The call chain behind the traceback

mainlevel_1level_2level_3level_1()level_2()level_3()problem[0] 💥IndexErrorIndexErrorIndexError

Figure 1: Calls go left to right (solid); when level_3 errors, the exception propagates back up the chain (dashed). The traceback lists this same chain, which is why the actual error sits at the bottom.

Handling Expected Errors

Use try-except blocks to handle errors gracefully and get better error information.

# Provide specific error messages for each failure mode
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: one success and one of each failure
safe_divide(10, 2)
safe_divide(10, 0)
safe_divide("10", 2)

# 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")
Expected Output:
Attempting to divide 10 by 2
Success: 10 / 2 = 5.0
Attempting to divide 10 by 0
Error: Cannot divide 10 by zero
Attempting to divide 10 by 2
Error: Invalid types for division
  a = '10' (type: str)
  b = 2 (type: int)
  Original error: unsupported operand type(s) for /: 'str' and 'int'
❌ 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
# Expected: $45 (20*2 + 5*3 + -10*1 = 40 + 15 - 10 = 45)
result = calculate_cart_total(cart)
print(f"Total: ${result}")  # this case 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)
print(f"\nBuggy total: ${result}")  # expected $51: the Book has 10% off

# 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
# Book with 10% discount: 20*2*0.9 = 36, Pen with none: 5*3 = 15, total 51
result = calculate_cart_total_fixed(cart2)
print(f"Fixed total: ${result}")
Expected Output:
Total: $45
Starting calculation with 2 items

  Item 0: Book
    price: 20
    quantity: 2
    item_total: 40
    running total: 40

  Item 1: Pen
    price: 5
    quantity: 3
    item_total: 15
    running total: 55

Buggy total: $55
Fixed total: $51.0

Binary Search Debugging

When you have a large codebase, use binary search to quickly narrow down where the bug is.

# A longer function that parses "name,age,city" into a record.
# Bug report: the age always comes out 10x too big ("30" becomes 300).
def process_user_data(user_input):
    parts = user_input.split(",")      # step 1
    if len(parts) != 3:                # step 2
        return None

    name = parts[0].strip()            # step 3
    age_str = parts[1].strip()         # step 4
    city = parts[2].strip()            # step 5
    age = int(age_str) * 10            # step 6

    return {                           # step 7
        "name": name.title(),
        "age": age,
        "city": city.title()
    }

result = process_user_data("alice,30,new york")
print(result)  # age comes out wrong

# Idea: instead of eyeballing all 7 steps, drop ONE checkpoint in the
# MIDDLE. Whatever side is still correct, you can ignore. Then repeat on
# the half that's wrong. Each checkpoint roughly halves the search space.

# Checkpoint after step 5 (the midpoint): are the parsed pieces correct?
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()

    print(f"🔍 CHECKPOINT: name={name!r}, age_str={age_str!r}, city={city!r}")

    age = int(age_str) * 10
    return {"name": name.title(), "age": age, "city": city.title()}

process_user_data("alice,30,new york")
# Parsing is correct (age_str is '30'), so the bug is in the SECOND half:
# the conversion or the formatting.

# Now bisect the second half: check the value right after conversion.
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) * 10
    print(f"🔍 CHECKPOINT 2: age={age}")

    return {"name": name.title(), "age": age, "city": city.title()}

process_user_data("alice,30,new york")

# age_str was '30' but age is 300, so the bug is the "* 10" on step 6.

# Fix: remove the stray "* 10"
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)  # fixed
    return {"name": name.title(), "age": age, "city": city.title()}

print(process_user_data("alice,30,new york"))

# Two checkpoints located a bug in a 7-step function. On a 100-line
# function, that's the difference between minutes and hours.
Expected Output:
{'name': 'Alice', 'age': 300, 'city': 'New York'}
🔍 CHECKPOINT: name='alice', age_str='30', city='new york'
🔍 CHECKPOINT 2: age=300
{'name': 'Alice', 'age': 30, 'city': 'New York'}

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)

result = count_vowels("HELLO WORLD")
print(result)  # expected 3 again, but watch what happens

# 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)

# The act of explaining forces you to think through each step
# and often reveals assumptions you didn't realize you made!
Expected Output:
3
0
3

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 the isolated tests. test_file_reading() is left out here because it
# needs a real sales.csv on disk; the other two carry their own data.
test_csv_parsing()
test_calculation()

# This quickly shows which part has the bug!
# Much easier than debugging everything at once.
Expected Output:
Lines: ['name,id,price,quantity', 'Widget,1,10.50,2', 'Gadget,2,25.00,1']
Headers: ['name', 'id', 'price', 'quantity']
Rows: [['Widget', '1', '10.50', '2'], ['Gadget', '2', '25.00', '1']]
Processing row: ['Widget', '1', '10.50', '2']
  price=10.5, quantity=2, revenue=21.0
Processing row: ['Gadget', '2', '25.00', '1']
  price=25.0, quantity=1, revenue=25.0
Total revenue: 46.0
Average sale: 23.0

Common Debugging Pitfalls

# Pitfall 1: Mutable default arguments
def add_item(item, items=[]):  # DON'T DO THIS!
    items.append(item)
    return items

# Bug: the default list is created once and persists between calls
list1 = add_item("apple")
print(list1)

list2 = add_item("banana")  # you would expect ['banana']
print(list2)

# 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()
print(f"Outside: {x}")  # 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)          # not quite 0.3
print(result == 0.3)

# Fix: Use approximate comparison
print(abs(result - 0.3) < 0.0001)

# Pitfall 4: Using 'is' instead of '=='
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)  # same contents
print(a is b)  # two separate list objects in memory

# '==' checks equality (same value), 'is' checks identity (same object).
# Use 'is' only for singletons like None: "if value is None:"

# 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!

# It looks right here, but only by luck. Removing an item shifts everything
# after it back one slot, and the loop's index has already moved on, so the
# next item is skipped. Run the same loop on [2, 4, 6] and it returns [4].
print(numbers)

# Fix: Build a new list instead of mutating the one you are walking
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers)
Expected Output:
['apple']
['apple', 'banana']
Inside: 20
Outside: 10
0.30000000000000004
False
True
True
False
[1, 3, 5]
[1, 3, 5]

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! 🚀