7. Chapter 7: Python Exception Handling¶
7.1. Python Exceptions¶
7.1.1. 1. What is an Exception¶
An exception is an error that occurs during program execution and disrupts normal program flow:
print(10 / 0) # Raises ZeroDivisionError
Without handling, the program terminates immediately.
7.1.2. 2. Basic try…except Block¶
The try block contains risky code; except handles the error gracefully:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
7.1.3. 3. Handling Multiple Exceptions¶
Different exception types can be handled separately:
try:
value = int("abc")
except ValueError:
print("Invalid conversion")
except ZeroDivisionError:
print("Division error")
7.1.4. 4. Using else with try…except¶
The else block executes only if no exception is raised:
try:
num = int("10")
except ValueError:
print("Conversion failed")
else:
print("Conversion successful:", num)
7.1.5. 5. Using finally Block¶
finally always executes, whether an exception occurs or not:
try:
file = open("data.txt", "r")
except FileNotFoundError:
print("File not found")
finally:
print("Execution completed")
7.1.6. 6. Catching All Exceptions (Generic Exception)¶
Catches any exception; useful for logging but should be used cautiously:
try:
risky_operation()
except Exception as e:
print("Error occurred:", e)
7.1.7. 7. Raising Custom Exceptions¶
The raise keyword triggers an exception manually:
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
validate_age(-5)
7.1.8. 8. Creating Custom Exception Classes¶
Custom exceptions improve clarity in domain-specific logic:
class NegativeNumberError(Exception):
pass
def check_number(n):
if n < 0:
raise NegativeNumberError("Negative numbers are not allowed")
check_number(-3)
7.1.9. 9. Using Assertions (assert)¶
Assertions are used for debugging and validation during development:
age = 15
assert age >= 18, "User must be at least 18 years old"
7.1.10. 10. Best Practice: Structured Exception Handling¶
Encapsulating exception logic improves reliability and user experience:
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "Division by zero error"
except TypeError:
return "Invalid input type"
print(divide(10, 0))
7.2. Python Exception Handling¶
7.2.1. 1. Purpose of Exception Handling¶
Exception handling prevents abrupt program termination and allows controlled recovery from runtime errors:
try:
result = 10 / 0
except ZeroDivisionError:
print("Handled division by zero safely")
Ensures stable application behavior.
7.2.2. 2. Basic try…except Structure¶
Separates risky code from error-handling logic:
try:
number = int("abc")
except ValueError:
print("Invalid number format")
7.2.3. 3. Handling Multiple Exceptions¶
Different exception types can be managed independently:
try:
value = int("10")
result = value / 0
except ValueError:
print("Conversion error")
except ZeroDivisionError:
print("Division error")
7.2.4. 4. Using try…except…else¶
else executes only if no exception occurs:
try:
num = int("50")
except ValueError:
print("Conversion failed")
else:
print("Conversion successful:", num)
7.2.5. 5. Using try…except…finally¶
finally always runs, making it ideal for resource cleanup:
try:
file = open("data.txt", "r")
except FileNotFoundError:
print("File not found")
finally:
print("Cleanup operations executed")
7.2.6. 6. Nested Exception Handling¶
Allows granular control over layered risky operations:
try:
try:
x = int("abc")
except ValueError:
print("Inner exception handled")
except Exception:
print("Outer exception handler")
7.2.7. 7. Catching Generic Exceptions¶
Catches all exceptions; useful for logging but should be used carefully:
try:
risky_operation()
except Exception as e:
print("Error:", e)
7.2.8. 8. Re-raising Exceptions¶
Preserves the original traceback while adding contextual handling:
try:
value = int("xyz")
except ValueError:
print("Logging error before re-raising")
raise
7.2.9. 9. Custom Exception Handling Strategy¶
Allows domain-specific error control:
class InvalidAgeError(Exception):
pass
def validate_age(age):
if age < 18:
raise InvalidAgeError("Age must be 18 or above")
try:
validate_age(16)
except InvalidAgeError as e:
print(e)
7.2.10. 10. Production-Grade Exception Handling Pattern¶
Combines graceful handling, error reporting, and guaranteed execution logic:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
return f"Error: {e}"
except Exception as e:
return f"Unexpected error: {e}"
finally:
print("Operation attempted")
print(safe_divide(10, 0))
7.3. Python Custom Exceptions¶
7.3.1. 1. Why Use Custom Exceptions¶
Custom exceptions improve clarity, maintainability, and semantic accuracy by representing domain-specific error conditions:
class InvalidInputError(Exception):
pass
Defines a new exception type tailored to your application logic.
7.3.2. 2. Basic Custom Exception Example¶
Raises a user-defined exception with a meaningful message:
class NegativeValueError(Exception):
pass
def process_value(value):
if value < 0:
raise NegativeValueError("Negative values are not allowed")
process_value(-10)
7.3.3. 3. Extending Exception with Custom Message¶
Adds dynamic context to error messages:
class AgeLimitError(Exception):
def __init__(self, age):
super().__init__(f"Age {age} is below allowed limit")
def validate_age(age):
if age < 18:
raise AgeLimitError(age)
validate_age(15)
7.3.4. 4. Catching Custom Exceptions¶
Handled like any built-in exception:
class AuthenticationError(Exception):
pass
try:
raise AuthenticationError("Invalid credentials")
except AuthenticationError as e:
print("Login failed:", e)
7.3.5. 5. Creating Hierarchy of Custom Exceptions¶
Facilitates organized error taxonomy and layered handling:
class ApplicationError(Exception):
pass
class DatabaseError(ApplicationError):
pass
class NetworkError(ApplicationError):
pass
7.3.6. 6. Custom Exception with Additional Attributes¶
Carries structured metadata for better diagnostics:
class TransactionError(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(f"[{code}] {message}")
raise TransactionError(403, "Unauthorized action")
7.3.7. 7. Using Custom Exceptions in Functions¶
Enforces logical validation constraints:
class FileMissingError(Exception):
pass
def load_config(filename):
if not filename.endswith(".json"):
raise FileMissingError("Only JSON configuration files supported")
load_config("config.txt")
7.3.8. 8. Custom Exception with Logging¶
Useful for application-level logging and audit trails:
class DataValidationError(Exception):
pass
try:
raise DataValidationError("Invalid CSV column format")
except DataValidationError as e:
print("Validation Error Logged:", e)
7.3.9. 9. Chaining Custom Exceptions¶
Preserves original error context using exception chaining:
class InitialError(Exception):
pass
class DerivedError(Exception):
pass
try:
try:
raise InitialError("Initial failure")
except InitialError as e:
raise DerivedError("Follow-up failure") from e
except DerivedError as final_error:
print(final_error)
7.3.10. 10. Best Practice: Naming and Design Pattern¶
Follow naming conventions:
Use meaningful names
Inherit from Exception
Include descriptive docstrings
class InvalidOrderStateError(Exception):
"""Raised when an order is in an invalid processing state"""
pass
def process_order(status):
if status != "confirmed":
raise InvalidOrderStateError("Order must be confirmed before processing")
process_order("draft")