Basic Exception Handling (try, except, finally)

Python uses try, except, and finally blocks to handle errors and exceptions gracefully, allowing your program to continue running even when an error occurs.

  • try Block: Contains the code that might raise an exception.
  • except Block: Catches and handles the exception if it occurs.
  • finally Block: Contains code that will always execute, whether an exception occurred or not.

Example:

  try:
    x = 10 / 0  # This will raise a ZeroDivisionError
except ZeroDivisionError:
    print("You can't divide by zero!")
finally:
    print("This will run no matter what.")
  

Custom Exceptions

You can create your own exceptions by defining a new class that inherits from the built-in Exception class. Custom exceptions are useful when you need to handle specific errors in your application.

Example:

  class CustomError(Exception):
    pass

def check_value(value):
    if value < 0:
        raise CustomError("Value cannot be negative!")

try:
    check_value(-1)
except CustomError as e:
    print(e)
  

Debugging Techniques

Debugging is the process of identifying and fixing errors in your code. Python provides several tools and techniques for debugging.

  • Using Print Statements: One of the simplest ways to debug is by adding print statements to check the values of variables at different points in your code.
  x = 5
print(f"x before loop: {x}")
for i in range(10):
    x += i
    print(f"x after adding {i}: {x}")
  
  • Using assert Statements: The assert statement tests if a condition is true. If it is not, the program will raise an AssertionError.
  def divide(a, b):
    assert b != 0, "Division by zero is not allowed!"
    return a / b
  
  • Using a Debugger: Python’s built-in pdb module allows you to set breakpoints, step through your code, and inspect variables interactively.
  import pdb
pdb.set_trace()
  
  • Using IDE Debugging Tools: Many IDEs (e.g., PyCharm, Visual Studio Code) provide advanced debugging tools that allow you to set breakpoints, watch variables, and step through your code line by line.

Handling Multiple Exception Types

  try:
    result = int(user_input) / divisor
except (ValueError, ZeroDivisionError) as e:
    print(f"Invalid operation: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
    raise  # re-raise after logging
  

else and finally Clauses

  try:
    file = open('data.txt')
except FileNotFoundError:
    print('File missing')
else:
    print(file.read())  # runs only if no exception
    file.close()
finally:
    print('Cleanup always runs')
  

Exception Chaining

Preserve the original cause when wrapping errors:

  try:
    config = json.loads(raw)
except json.JSONDecodeError as e:
    raise ConfigError('Invalid config file') from e
  

Logging Exceptions

  import logging

logger = logging.getLogger(__name__)

try:
    process_order(order_id)
except OrderError as e:
    logger.exception('Order %s failed', order_id)
    raise
  

logger.exception() automatically includes the stack trace.

When to Use Exceptions vs Return Values

Situation Approach
Expected failure (file not found) Return None or Result type
Programming bug (index out of range) Let exception propagate
User input validation Raise ValueError with clear message
API errors Custom exception with status code