Ojasa Mirai

Ojasa Mirai

Python

Loading...

Learning Level

🟒 BeginnerπŸ”΅ Advanced
Exceptions OverviewException TypesTry-Except BlocksRaising ExceptionsCustom ExceptionsMultiple ExceptionsFinally & CleanupDebugging TechniquesLogging Best Practices
Python/Error Handling/Debugging Techniques

πŸ› Debugging Techniques β€” Finding and Fixing Errors

Learn practical methods to understand what's going wrong in your code.


πŸ“ Reading Stack Traces

When an exception occurs, Python shows you a traceback. Learn to read it:

# Example: A typical traceback
def process_user(user_dict):
    age = int(user_dict["age"])
    next_year_age = age + 1
    return next_year_age

def main():
    users = [
        {"name": "Alice", "age": "25"},
        {"name": "Bob"},  # Missing age!
    ]

    for user in users:
        result = process_user(user)
        print(f"Next year age: {result}")

main()

# Output:
# Traceback (most recent call last):
#   File "script.py", line 14, in <module>
#     main()
#   File "script.py", line 12, in main
#     result = process_user(user)
#   File "script.py", line 2, in process_user
#     age = int(user_dict["age"])
# KeyError: 'age'

# Read it from bottom to top:
# 1. KeyError: 'age' - the actual error
# 2. In process_user() - which function
# 3. age = int(user_dict["age"]) - which line
# 4. Called from main() - where was it called from

πŸ–¨οΈ Print Debugging

Simple but effectiveβ€”add print statements to understand flow:

# Example: Finding where code breaks
def calculate_average(numbers):
    print(f"Input: {numbers}")
    total = sum(numbers)
    print(f"Total: {total}")
    count = len(numbers)
    print(f"Count: {count}")
    average = total / count
    print(f"Average: {average}")
    return average

# Test with problem input
result = calculate_average([])  # Empty list!

# Output shows that count is 0, leading to division by zero
# Input: []
# Total: 0
# Count: 0
# ZeroDivisionError: division by zero

# Fixed version with debugging
def calculate_average(numbers):
    print(f"Starting calculation with {len(numbers)} numbers")

    if not numbers:
        print("WARNING: Empty list provided")
        return None

    total = sum(numbers)
    print(f"Sum calculated: {total}")

    average = total / len(numbers)
    print(f"Average calculated: {average}")

    return average

result = calculate_average([])
print(f"Result: {result}")

# Output:
# Starting calculation with 0 numbers
# WARNING: Empty list provided
# Result: None

πŸ” Exception Information

Extract and examine exception details:

# Access exception information in except block
import traceback

def risky_operation():
    data = [1, 2, 3]
    return data[10]  # Index error

try:
    risky_operation()
except IndexError as e:
    # Get the exception message
    print(f"Error: {e}")

    # Get the exception type
    print(f"Type: {type(e).__name__}")

    # Get full traceback as string
    print("Full traceback:")
    print(traceback.format_exc())

# Output:
# Error: list index out of range
# Type: IndexError
# Full traceback:
# Traceback (most recent call last):
#   File "script.py", line 8, in <module>
#     ...

πŸ§ͺ Assertion Statements

Use assertions to catch logic errors:

# Assert stops execution if condition is false
def validate_age(age):
    assert isinstance(age, int), "Age must be integer"
    assert age >= 0, "Age cannot be negative"
    assert age <= 150, "Age seems unrealistic"
    return True

# Good input
validate_age(25)  # Passes silently

# Bad input
try:
    validate_age("25")  # String instead of int
except AssertionError as e:
    print(f"Assertion failed: {e}")

# Output: Assertion failed: Age must be integer

# Use in debugging
def process_payment(amount, balance):
    # Sanity checks
    assert amount > 0, "Amount must be positive"
    assert balance >= 0, "Balance cannot be negative"
    assert amount <= balance, "Insufficient funds"

    return balance - amount

# Test
try:
    new_balance = process_payment(150, 100)
except AssertionError as e:
    print(f"Payment failed: {e}")

# Output: Payment failed: Insufficient funds

πŸ”— Debugging Workflow

# Step 1: Understand the error
# Read the traceback from bottom to top

# Step 2: Find the problem
def fetch_user_name(user_data):
    # What if user_data is None?
    return user_data["name"].upper()

try:
    result = fetch_user_name(None)
except TypeError as e:
    print(f"Error: {e}")  # Output: 'NoneType' object is not subscriptable

# Step 3: Add debug info
def fetch_user_name_debug(user_data):
    print(f"DEBUG: user_data = {user_data}")
    print(f"DEBUG: type = {type(user_data)}")

    if user_data is None:
        print("DEBUG: user_data is None!")
        return None

    if "name" not in user_data:
        print("DEBUG: 'name' key missing!")
        return None

    name = user_data["name"]
    print(f"DEBUG: Found name = {name}")
    return name.upper()

result = fetch_user_name_debug(None)

# Step 4: Fix the issue
def fetch_user_name_fixed(user_data):
    if not user_data:
        return None

    return user_data.get("name", "Unknown").upper()

🐍 Using pdb (Python Debugger)

The interactive debugger lets you pause and inspect code:

# Add pdb.set_trace() to pause execution
import pdb

def process_data(items):
    result = []
    for i, item in enumerate(items):
        pdb.set_trace()  # Execution pauses here
        # Now you can inspect variables:
        # > p item         - print item
        # > p i            - print i
        # > p result       - print result
        # > n              - next line
        # > c              - continue
        # > l              - list code

        processed = item * 2
        result.append(processed)

    return result

# Step through manually
data = [1, 2, 3]
# result = process_data(data)

# Better: use conditional breakpoint
def process_data_conditional(items):
    for item in items:
        if item == 2:  # Only stop for specific value
            import pdb; pdb.set_trace()

        print(f"Processing {item}")

# data = [1, 2, 3]
# result = process_data_conditional(data)

🧹 Debug Output vs Logging

# ❌ Bad: debug prints left in production code
def calculate(a, b):
    print(f"DEBUG: a={a}, b={b}")  # Never should go to production!
    result = a + b
    print(f"DEBUG: result={result}")
    return result

# βœ… Better: use logging
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def calculate_safe(a, b):
    logger.debug(f"Calculating: a={a}, b={b}")
    result = a + b
    logger.debug(f"Result: {result}")
    return result

# Logging output can be turned on/off by severity level
calculate_safe(5, 3)

🎯 Debugging Strategies

# Strategy 1: Verify assumptions
def find_user(users_list, user_id):
    # Assumption: users_list is not empty
    # Assumption: user_id exists in list

    # Debug: check assumptions
    print(f"DEBUG: Searching {len(users_list)} users for ID {user_id}")

    for user in users_list:
        if user["id"] == user_id:
            return user

    print(f"DEBUG: User {user_id} not found")
    return None

# Strategy 2: Test one thing at a time
def complex_calculation(a, b, c):
    step1 = a + b
    print(f"Step 1: {a} + {b} = {step1}")

    step2 = step1 * c
    print(f"Step 2: {step1} * {c} = {step2}")

    return step2

# Strategy 3: Simplify the test case
# Instead of running the full program, test the broken function in isolation
def buggy_function(data):
    return data[0] + data[1]

# Create minimal test case
try:
    result = buggy_function([])  # Empty list reveals the bug
except IndexError:
    print("Index error with empty list!")

# Strategy 4: Binary search (comment out code)
# If you have lots of code, comment out the second half
# If error still happens, bug is in first half
# If error goes away, bug is in second half

πŸ’‘ Common Debugging Scenarios

# Scenario 1: "It works sometimes"
# Usually indicates: uninitialized variables, type mismatch, or race conditions

def unreliable_function(items):
    if len(items) > 0:
        return items[0]
    # BUG: What if items is None?

# Debug: add type check
def reliable_function(items):
    if items is None:
        print("DEBUG: items is None!")
        return None
    if len(items) == 0:
        return None
    return items[0]

# Scenario 2: "Wrong result"
# Usually indicates: logic error, wrong variable, or incorrect calculation

def calculate_discount(price, discount_percent):
    # Bug: forgot to divide discount by 100
    return price - discount_percent  # Should be: discount_percent / 100

# Debug: print intermediate values
def calculate_discount_debug(price, discount_percent):
    discount_amount = (price * discount_percent) / 100
    print(f"DEBUG: Price={price}, Discount%={discount_percent}, Amount={discount_amount}")
    return price - discount_amount

# Scenario 3: "Function returns None"
# Check: is there a return statement? Does every path return?

def get_greeting(time_of_day):
    if time_of_day == "morning":
        return "Good morning!"
    elif time_of_day == "evening":
        return "Good evening!"
    # BUG: no return for other times!

# Debug: check all code paths
def get_greeting_fixed(time_of_day):
    if time_of_day == "morning":
        return "Good morning!"
    elif time_of_day == "evening":
        return "Good evening!"
    else:
        return "Hello!"  # Handle all cases

🎯 Key Takeaways

  • βœ… Read stack traces from bottom to top
  • βœ… Use print statements strategically
  • βœ… Use assertions for sanity checks
  • βœ… Use `pdb.set_trace()` for interactive debugging
  • βœ… Extract exception information for better error messages
  • βœ… Test edge cases (empty, None, wrong type)
  • βœ… Verify your assumptions about data


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep β€’ Build Real β€’ Verify Skills β€’ Launch Forward

Courses

PythonFastapiReactJSCloud

Β© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn