Monday, October 23, 2023

Demystifying Python Exceptions: From the Basics to Advanced Exception Handling 📖

  • Python is a forgiving language, but even the best code can encounter unexpected errors. In this comprehensive guide, we'll dive into exceptions and exception handling. 
  • Starting with the fundamentals, we'll explore advanced techniques for robust code that can handle anything life throws at it.

1. Understanding Exceptions

  • Exceptions are runtime errors that interrupt the normal execution flow. They are objects that contain information about what went wrong.

2. Common Exceptions

  • These are errors frequently encountered, such as ZeroDivisionError when dividing by zero or ValueError when attempting to convert a non-numeric string to a number.

result = 10 / 0

3. Basic Exception Handling

  • Basic exception handling involves using a try and except block to catch and manage exceptions.

try: result = 10 / 0 except ZeroDivisionError: print("You can't divide by zero!")

4. The try and except Block

  • The try block contains code that may raise exceptions, and the except block contains code to handle those exceptions.

try:
result = 10 / 0
except ZeroDivisionError:
print("You can't divide by zero!")

5. Handling Multiple Exceptions

  • In some cases, you may need to handle different exceptions separately within a single try block.

try: value = int(input("Enter a number: ")) result = 10 / value except (ValueError, ZeroDivisionError) as e: print(f"An error occurred: {e}")

6. Advanced Exception Handling Techniques

  • Advanced techniques include raising exceptions using the raise statement and defining custom exceptions for specific scenarios.

def divide(a, b): if b == 0: raise ZeroDivisionError("You can't divide by zero!") return a / b

7. User-Defined Exceptions

  • Python allows you to create your own exception classes to provide more detailed error messages.

class CustomError(Exception): def __init__(self, message): super().__init__(message) try: raise CustomError("This is a custom error.") except CustomError as e: print(e)

8. The finally Block

  • The finally block contains code that always executes, whether an exception occurs or not.

try: result = 10 / 0 except ZeroDivisionError: print("You can't divide by zero!") finally: print("Execution completed.")

9. Using else with try and except

  • The else block runs if no exceptions occur within the try block, allowing for code optimization.

try: value = int(input("Enter a number: ")) except ValueError: print("Invalid input. Please enter a number.") else: print(f"You entered: {value}")

10.Exercise_1 - Python Exceptions Tracker Application

Project Description:

  • Python Exceptions  Tracker Application is designed to demonstrate various aspects of exception handling in Python.
  • It provides a menu-driven interface for users to explore common exceptions, practice advanced exception handling, define and raise custom exceptions, view the exception history, and save the exception history to a file.

2. Features 

  • The application includes the following features:

2.1. Handle Common Exceptions 

  • Users can select from a list of common exceptions. The application will raise the chosen exception and display its description and message.

2.2. Practice Advanced Exception Handling 

  • Users can input a number. The application will perform a division operation and handle exceptions like ZeroDivisionError and ValueError.

2.3. Define and Raise Custom Exceptions 

  • Users can define and raise a custom exception. The application will handle and display the custom exception.

2.4. View Exception History 

  • Users can view the history of exceptions that have been raised.

2.5. Save Exception History to File 

  • Users can save the exception history to a user-specified file.

3. Usage 

  • Run the application. Select an option from the menu (1/2/3/4/5/6). Follow the prompts for each option. Explore and understand Python exception handling.

4. Implementation Details 

  1. The application is implemented in Python and consists of several functions: 
  2. handle_common_exceptions(): Allows the user to select and raise common exceptions. 
  3. raise_exception() : Raises a specific exception and provides details. 
  4. practice_advanced_exception_handling(): Demonstrates advanced exception handling.
  5. define_and_raise_custom_exception(): Defines and raises a custom exception. 
  6. view_exception_history(): Displays the history of raised exceptions. 
  7. save_exception_history_to_file(): Saves the exception history to a user-specified file. 
  8. global_exception_handler(): Handles unhandled exceptions and records them. 
  9. record_exception(): Records exceptions in the exception history.

Below is  code for the Python  Exceptions Tracker Application that meets the above specified requirements

# Python Exceptions Tracker Application
import traceback
import sys
# List to store the history of exceptions
exception_history = []

# Dictionary to store information about common exceptions
common_exceptions = {
'ZeroDivisionError': 'Occurs when dividing by zero.',
'ValueError': 'Occurs when an invalid value is provided to a function.',
'TypeError': 'Occurs when an operation is performed on an inappropriate data type.',
'FileNotFoundError': 'Occurs when attempting to access a file that does not exist.',
# Add more common exceptions and descriptions as needed
}

# Function to handle common exceptions
def handle_common_exceptions():
while True:
print("Common Exceptions:")
for index, exception in enumerate(common_exceptions, start=1):
print(f"{index}. {exception}")

choice = input("Select an exception (1/2/3/4/... or 'q' to quit): ")
if choice == 'q':
break
try:
index = int(choice) - 1
selected_exception = list(common_exceptions.keys())[index]
raise_exception(selected_exception)
except (ValueError, IndexError):
print("Invalid choice. Please select a valid option or 'q' to quit.")

# Function to raise a specific exception
def raise_exception(exception_name):
try:
if exception_name == 'ZeroDivisionError':
result = 1 / 0
elif exception_name == 'ValueError':
value = int("invalid")
elif exception_name == 'TypeError':
value = "string" + 1
elif exception_name == 'FileNotFoundError':
with open("non_existent_file.txt", "r") as file:
file.read()
# Add handling for additional common exceptions here
except Exception as e:
record_exception(e)
print(f"Exception: {exception_name}")
print(f"Description: {common_exceptions[exception_name]}")
print(f"Message: {str(e)}")

# Function to practice advanced exception handling
def practice_advanced_exception_handling():
try:
num = int(input("Enter a number: "))
result = 10 / num
print(f"Result: {result}")
except ZeroDivisionError as e:
record_exception(e)
print("Error: Division by zero is not allowed.")
except ValueError as e:
record_exception(e)
print("Error: Invalid input. Please enter a valid number.")
except Exception as e:
record_exception(e)
print(f"An unexpected error occurred: {str(e)}")

# Function to define and raise a custom exception
class CustomException(Exception):
pass

def define_and_raise_custom_exception():
try:
raise CustomException("This is a custom exception.")
except CustomException as e:
record_exception(e)
print(f"Custom Exception Raised: {str(e)}")

# Function to view the exception history
def view_exception_history():
if not exception_history:
print("No exceptions in the history.")
else:
print("Exception History:")
for index, exception in enumerate(exception_history, start=1):
print(f"{index}. {exception}")

# Function to save the exception history to a file
def save_exception_history_to_file():
if not exception_history:
print("No exceptions to save. Exception history is empty.")
else:
filename = input("Enter the filename to save the exception history: ")
try:
with open(filename, 'w') as file:
file.write("\n".join(exception_history))
print(f"Exception history saved to {filename}.")
except Exception as e:
print(f"Error while saving the exception history: {str(e)}")

# Function to catch unhandled exceptions globally
def global_exception_handler(exctype, value, tb):
formatted_traceback = traceback.format_exception(exctype, value, tb)
exception_message = "".join(formatted_traceback)
record_exception(exception_message)
print("An unhandled exception occurred:")
print(exception_message)

# Function to record exceptions in the history
def record_exception(exception):
exception_history.append(str(exception))

# Set the global exception handler
sys.excepthook = global_exception_handler

# Main application loop
while True:
print("\nPython Exceptions Application")
print("1. Handle Common Exceptions")
print("2. Practice Advanced Exception Handling")
print("3. Define and Raise Custom Exceptions")
print("4. View Exception History")
print("5. Save Exception History to File")
print("6. Exit")

choice = input("Enter your choice (1/2/3/4/5/6): ")

if choice == '1':
handle_common_exceptions()
elif choice == '2':
practice_advanced_exception_handling()
elif choice == '3':
define_and_raise_custom_exception()
elif choice == '4':
view_exception_history()
elif choice == '5':
save_exception_history_to_file()
elif choice == '6':
print("Exiting the Python Exceptions Application. Goodbye!")
break
else:
print("Invalid choice. Please select a valid option (1/2/3/4/5/6).")

11.Exercise_2 - Finance Tracker Application

Project Description: 

  • The Personal Finance Tracker is a Python application designed to help users manage their financial transactions, budgets, and overall financial health. 
  • It allows users to record transactions, view their financial history, calculate their total balance, set budget limits for different expense categories, and check if their spending exceeds budget limits.

Features

1. Add Transaction 

  • Users can enter details of a financial transaction, including the date, description, category, and amount. Input: Date (YYYY-MM-DD), Description, Category, Amount. Output: Transaction added successfully.

2. View Transactions

  • Users can view a list of all their recorded transactions. Input: None. Output: A list of transactions with details including date, description, category, and amount.

3. Calculate Total Balance:  

  • Users can calculate their total balance, which is the difference between their total income and total expenses. Input: None. Output: The total balance is displayed.

4. Calculate Category Spending

  • Users can check the total spending within a specific category. Input: Category name. Output: The total spending within the specified category is displayed.

5. Set Budget

  • Users can set budget limits for different expense categories. Input: Category name, Budget limit (amount). Output: Budget set successfully.

6. View Budgets

  • Users can view a list of all budget limits set for different categories. Input: None. Output: A list of categories and their associated budget limits.

7. Check Budget

  • Users can check if their spending within a specific category exceeds the budget limit. Input: Category name. Output: The application informs the user if the spending has exceeded the budget or is within the budget.

8. Save Data

  • Users can save their transaction history and budget data to a JSON file for future reference. Input: Filename for data storage. Output: Confirmation of data saved.

9. Load Data

  • Users can load previously saved transaction history and budget data from a JSON file. Input: Filename from which to load data. Output: Confirmation of data loaded.

Assumptions and Constraints 

  • Users are expected to provide valid data in the specified format (e.g., valid date format, numeric amounts). Budget limits can be set or checked for specific categories. 
  • If a category is not defined, it is considered as having no budget limit.

Error Handling 

  • The application handles errors such as invalid amounts and non-existent files when saving or loading data. The application handles cases where users attempt to view transactions or calculate total balance with no recorded transactions. 
  • The application informs the user when attempting to calculate category spending for non-existent or zero-spending categories.

import json

class Transaction:
def __init__(self, date, description, category, amount):
self.date = date
self.description = description
self.category = category
self.amount = amount


class PersonalFinanceTracker:
def __init__(self):
self.transactions = []
self.budgets = {}

def add_transaction(self, transaction):
if not isinstance(transaction.amount, (int, float)):
raise ValueError("Amount must be a number.")
self.transactions.append(transaction)

def view_transactions(self):
for transaction in self.transactions:
print(
f"Date: {transaction.date}, Description: {transaction.description},
                Category: {transaction.category}, Amount: {transaction.amount}")

def calculate_total_balance(self):
income = sum(transaction.amount for transaction in self.transactions if transaction.amount > 0)
expenses = sum(transaction.amount for transaction in self.transactions if transaction.amount < 0)
return income - expenses

def calculate_category_spending(self, category):
category_expenses = sum(
transaction.amount for transaction in self.transactions if transaction.category == category)
return category_expenses

def set_budget(self, category, budget_limit):
self.budgets[category] = budget_limit

def view_budgets(self):
for category, budget_limit in self.budgets.items():
print(f"Category: {category}, Budget Limit: {budget_limit}")

def check_budget(self, category):
if category in self.budgets:
category_expenses = sum(
transaction.amount for transaction in self.transactions if transaction.category == category)
budget_limit = self.budgets[category]
return category_expenses, budget_limit
else:
return None, None

def save_data(self, filename):
data = {
"transactions": [
{
"date": transaction.date,
"description": transaction.description,
"category": transaction.category,
"amount": transaction.amount,
}
for transaction in self.transactions
],
"budgets": self.budgets
}
with open(filename, "w") as file:
json.dump(data, file)
print(f"Data saved to '{filename}'.")

def load_data(self, filename):
try:
with open(filename, "r") as file:
data = json.load(file)
self.transactions = [
Transaction(
item["date"],
item["description"],
item["category"],
item["amount"],
)
for item in data.get("transactions", [])
]
self.budgets = data.get("budgets", {})
print(f"Data loaded from '{filename}'.")
except FileNotFoundError:
print("File not found. Unable to load data.")
except json.JSONDecodeError:
print("Invalid JSON format in the file. Unable to load data.")


def initialize_budgets(finance_tracker):
# Initialize budgets for different categories
finance_tracker.set_budget("Groceries", 300.0)
finance_tracker.set_budget("Rent", 1200.0)
finance_tracker.set_budget("Transportation", 150.0)
finance_tracker.set_budget("Entertainment", 200.0)


def initialize_data(finance_tracker):
# Initialize some transactions
transaction1 = Transaction("2023-01-05", "Groceries", "Food", -50.0)
transaction2 = Transaction("2023-01-10", "Paycheck", "Income", 1500.0)
transaction3 = Transaction("2023-01-15", "Rent", "Housing", -800.0)

finance_tracker.add_transaction(transaction1)
finance_tracker.add_transaction(transaction2)
finance_tracker.add_transaction(transaction3)

# Initialize some budgets
finance_tracker.set_budget("Food", 200.0)
finance_tracker.set_budget("Housing", 1000.0)


def main():
finance_tracker = PersonalFinanceTracker()

# Initialize data
initialize_data(finance_tracker)
# Initialize budgets
initialize_budgets(finance_tracker)

while True:
print("\nPersonal Finance Tracker")
print("1. Add Transaction")
print("2. View Transactions")
print("3. Calculate Total Balance")
print("4. Calculate Category Spending")
print("5. Set Budget")
print("6. View Budgets")
print("7. Check Budget")
print("8. Save Data")
print("9. Load Data")
print("10. Exit")

choice = input("Enter your choice (1/2/3/4/5/6/7/8/9/10): ")

if choice == '1':
date = input("Enter the date (YYYY-MM-DD): ")
description = input("Enter a description: ")
category = input("Enter a category: ")
try:
amount = float(input("Enter the amount: "))
transaction = Transaction(date, description, category, amount)
finance_tracker.add_transaction(transaction)
print("Transaction added successfully!")
except ValueError:
print("Invalid amount. Please enter a valid number.")

if choice == '2':
if finance_tracker.transactions:
finance_tracker.view_transactions()
else:
print("No transactions to display.")

elif choice == '3':
total_balance = finance_tracker.calculate_total_balance()
print(f"Total Balance: {total_balance}")

elif choice == '4':
category = input("Enter the category to calculate spending: ")
category_spending = finance_tracker.calculate_category_spending(category)

if category_spending is not None:
print(f"Spending in '{category}': {category_spending}")
else:
print(f"Category '{category}' does not exist or has no spending.")

elif choice == '5':
category = input("Enter the category to set a budget: ")
budget_limit = float(input("Enter the budget limit: "))
finance_tracker.set_budget(category, budget_limit)
print(f"Budget set for '{category}' with a limit of {budget_limit}")

elif choice == '6':
finance_tracker.view_budgets()

elif choice == '7':
category = input("Enter the category to check the budget: ")
category_expenses, budget_limit = finance_tracker.check_budget(category)
if category_expenses is not None:
if category_expenses > budget_limit:
print(f"Category '{category}' has exceeded its budget of {budget_limit}.")
else:
print(f"Category '{category}' is within its budget of {budget_limit}.")
else:
print(f"No budget set for category '{category}'.")

elif choice == '8':
filename = input("Enter the filename for data: ")
finance_tracker.save_data(filename)

elif choice == '9':
filename = input("Enter the filename to load data from: ")
finance_tracker.load_data(filename)

elif choice == '10':
print("Exiting the Personal Finance Tracker. Goodbye!")
break

else:
print("Invalid choice. Please select a valid option (1/2/3/4/5/6/7/8/9/10).")

if __name__ == "__main__":
main()

13.Exercise_3 Coffee Machine Simulation

14. Exception Best Practices

Follow these best practices to ensure your exception handling is effective and maintainable.
  • Use Specific Exceptions: Catch specific exceptions whenever possible. This helps pinpoint issues more accurately.
  • Avoid Broad except Blocks: Avoid using a generic except block that catches all exceptions. It can hide unexpected issues.
  • Log Exceptions: Use logging to record exceptions. This aids in debugging and diagnosing problems.
  • Handle Exceptions Appropriately: Ensure that your exception-handling code addresses the specific issue and does not mask other errors.
  • Keep It Simple: Don't overcomplicate exception handling. Complex structures can make code harder to understand.
  • Use Docstrings: Document custom exception classes with docstrings to provide clarity on their purpose and usage.
  • Test Exception Scenarios: Thoroughly test your code with various inputs to ensure exceptions are raised and handled correctly.
  • Follow PEP 8: Adhere to the Python Enhancement Proposal 8 (PEP 8) style guide for consistent and readable code.
  • Collaborate: When working on larger projects, discuss exception handling strategies with your team to maintain consistency.
  • Review and Refactor: Periodically review exception handling in your code and refactor if necessary to improve clarity and efficiency.
  • Keep It Well-Documented: Provide comments or documentation for complex exception-handling logic to aid future maintainers.
  • Avoid Overusing Exceptions: Exceptions should be used for exceptional cases. Avoid using them for control flow in your code.

Conclusion

  • Exception handling is an essential skill for Python programmers. With a deep understanding of how exceptions work and the ability to handle them gracefully, your code becomes robust and reliable. 
  • Embrace exceptions, and let your Python programs handle anything that comes their way. 📖🐍

You may also like

Kubernetes Microservices
Python AI/ML
Spring Framework Spring Boot
Core Java Java Coding Question
Maven AWS