Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Some Good Debugging Techniques

  • process of identifying & reselving errors / bugs

  • understand the state & flow of variables

def add(a,b):
  result = a + b
  print(f'{a} + {b} = {result}')
  # by printing a,b, result -> verify func. working as expected
  return result

print(add(4,5))
4 + 5 = 9
9

Interactive debugging with pdb module

pdb -> interactive debugging environment

import pdb

def divide(a,b):
  pdb.set_trace()
  #      ^
  #    func. -> step through the code
  #          -> inspect variables
  #          -> understand prog. behavior
  return a/b

print(divide(10,2))

# running will give
# 1. loc of the file, where the set_trace() is called
# 2. the code immidiately after it
# 3. prompt -> (pdb)
#     |-> (pdb) help  ---a list of commands
#     |-> (pdb) whatis -- understand the type of elements
#     |-> (pdb) continue / cont / c  -- continue execution of code
> /tmp/ipykernel_8582/2391405730.py(9)divide()
      7   #          -> inspect variables
      8   #          -> understand prog. behavior
----> 9   return a/b
     10 
     11 print(divide(10,2))

ipdb> c
5.0

IDE debugging tools

  • provide -> visual interface to examine -> state of program

  • easy to identify & fix issues

Advance debugging tools

  • breakpoints

  • setp execution

  • variable ispection

Using VS Code debugger

If you use VS Code, you can set breakpoints in your code and run the debugger to pause execution at those points. Here’s how to debug the same divide function:

Step 1: Set up your code

Create a file called main.py with the following content:

def divide(a, b):
    result = a / b
    return result

print(divide(10, 2))
print(divide(15, 3))

###Step 2: Set a breakpoint

  • Click in the gutter (left margin) next to line 2 (result = a / b) to set a breakpoint

  • A red dot will appear, indicating the breakpoint is set

###Step 3: Start debugging

  • Press F5 or go to Run > Start Debugging

  • Select “Python File” when prompted

  • The debugger will pause execution at your breakpoint

###Step 4: Inspect variables

  • Hover over variables to see their current values

  • Use the Variables panel on the left to see all local variables

  • Use the Debug Console at the bottom to evaluate expressions

###Step 5: Step through code

Use the debug toolbar to:

  • Continue (F5): Resume execution until the next breakpoint

  • Step Over (F10): Execute the current line and move to the next

  • Step Into (F11): Enter into function calls

  • Step Out (Shift+F11): Exit the current function

Exception Handling

process of catching and managing errors that occur during the execution of a program - so code does’t crash unexpectedly

For handling errors \rightarrow py has :

  1. try → block of code - where error might occur

  2. except → runs if an error of the specified type is raised inside the try block.

  3. else → Runs if no exception is raised in the try block.

  4. finally → Runs no matter what, whether or not an exception occurred.

    • Useful for Clean-up tasks :

      • closing files

      • releasing resources

try:
  x = 10 / 0
except ZeroDivisionError:
  print("You can't divide by zero!")
# dividing by zero raises a ZeroDivisionError,
# which is then caught and handled.
You can't divide by zero!
try :
  x = 10 / 2
except ZeroDivisionError:
  print("You can't divide by zero!")
else :
  print('Division successful:',x)
finally :
  print('This block always runs.')
Division successful: 5.0
This block always runs.

catch multiple exceptions with separate except blocks → make your error responses more specific and useful.

try :
  number = int ('abc')
  result = 10 / number
except ValueError :
#            ^
#         exception class
  print('That was not a valid number.')
except ZeroDivisionError:
#            ^
#         exception class
  print("Can't divide by zero.")
That was not a valid number.
try :
  x = 1 / 0
except ZeroDivisionError as e :
  # e -> Exception Object - an instance created from that class.
    print(f'Error occurred: {e}')
Error occurred: division by zero

multiple exceptions in a single except clause by specifying the exceptions as a tuple:

try :
  number = int(input('Enter a number: '))
  result = 10 / number
except (ValueError, ZeroDivisionError) as e:
  print(f'Error occurred:{e}')
Enter a number: 0
Error occurred:division by zero

Raise Statement

  • to manually trigger exceptions in your code

  • to signal that an error condition has occurred or that certain requirements haven’t been met.

usecases:

  • creating robust applications where you need to

    • enforce business rules

    • validate input

    • provide meaningful error messages

raise built-in exceptions or create custom error messages.

def check_age (age):
  if age < 0 :
    raise ValueError ('Age can\'t be negative')
#    ^        |-> ValueError with custom message
#   trig. an exception
  return age

try:
  check_age(-5)
except ValueError as e:
  print(f'Error: {e}')
Error: Age can't be negative

The raise statement can also be used to re-raise the current exception, which is particularly useful in exception handling → allows to log / perform cleanup while still propagating the error up the call stack.

def process_data(data):
    try:
        result = int(data)
        return result * 2
    except ValueError:
        print('Logging: Invalid data received')
        raise  # Re-raises the same ValueError
# keeps the exception alive and,
# pass it back out to wherever the function was called.

try:
    process_data('abc')
except ValueError:
    print('Handled at higher level')
Logging: Invalid data received
Handled at higher level

create and raise custom exceptions by defining your own exception classes:

class InsufficientFundsError(Exception):
  def __init__(self, balance, amount): # More study needed
    self.balance = balance
    self.amount = amount
    super().__init__(f'Insuffucient funds: ${balance} avaliable, ${amount} requested')

def withdraw(balance, amount):
  if amount > balance :
    raise InsufficientFundsError(balance, amount)
  return balance - amount

try:
  new_balance = withdraw(100,150)
except InsufficientFundsError as e:
  print(f'Transaction failed:{e}')
Transaction failed:Insuffucient funds: $100 avaliable, $150 requested

More Study Needed: The raise statement can also be used with the from keyword to chain exceptions, showing the relationship between different errors:

def parse_config(filename):
    try:
        with open(filename, 'r') as file:
            data = file.read()
            return int(data)
    except FileNotFoundError:
        raise ValueError('Configuration file is missing') from None
    except ValueError as e:
        raise ValueError('Invalid configuration format') from e

config = parse_config('config.txt')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_397/3068554.py in <cell line: 0>()
      9         raise ValueError('Invalid configuration format') from e
     10 
---> 11 config = parse_config('')

/tmp/ipykernel_397/3068554.py in parse_config(filename)
      5             return int(data)
      6     except FileNotFoundError:
----> 7         raise ValueError('Configuration file is missing') from None
      8     except ValueError as e:
      9         raise ValueError('Invalid configuration format') from e

ValueError: Configuration file is missing

We can also raise exceptions conditionally with assert statements, which are essentially shorthand for raise with AssertionError:

def calculate_square_root(number):
#   assert <condition>, <error_message>
    assert number >= 0, 'Cannot calculate square root of negative number'
    return number ** 0.5

try:
    result = calculate_square_root(-4)
    print(result)
except AssertionError as e:
    print(f'Assertion failed: {e}')
Assertion failed: Cannot calculate square root of negative number