1. Errors vs. Exceptions
While often used interchangeably in everyday language, in Python, "errors" and "exceptions" have distinct meanings:
-
Errors: These typically refer to problems that prevent the program from even starting or indicate a fundamental flaw in your code's syntax or structure. Examples include `SyntaxError`, `IndentationError`, `NameError` (if a variable isn't defined). You generally fix these by debugging your code.
-
Exceptions: These are events detected during the program's *execution* that disrupt the normal flow. They signal that something unexpected but potentially recoverable has happened. Exceptions can be *caught* and *handled*. Examples include `ValueError`, `TypeError`, `FileNotFoundError`, `ZeroDivisionError`, `IndexError`, `KeyError`.
Our focus in this module is primarily on managing **exceptions**.
2. The `try`, `except`, `else`, `finally` Blocks
This is the cornerstone of exception handling in Python.
Basic `try-except`
The `try` block contains the code that might raise an exception. If an exception occurs, the code inside the `except` block is executed.
# Example: Division by Zero
def safe_divide(a, b):
try:
result = a / b
print(f"Result of division: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
print("Division attempt finished.")
print("--- Scenario 1: Valid Division ---")
safe_divide(10, 2)
print("\n--- Scenario 2: Division by Zero ---")
safe_divide(10, 0)
# Example: Invalid input type
def get_positive_number():
try:
num_str = input("Enter a positive number: ")
number = int(num_str)
if number < 0:
raise ValueError("Number must be positive")
print(f"You entered: {number}")
except ValueError as e:
print(f"Invalid input: {e}. Please enter an integer.")
except Exception as e: # Catching any other unexpected errors
print(f"An unexpected error occurred: {e}")
print("\n--- Scenario 3: Valid Integer Input ---")
get_positive_number()
print("\n--- Scenario 4: Non-Integer Input ---")
get_positive_number()
print("\n--- Scenario 5: Negative Integer Input (custom check) ---")
get_positive_number()
$ python your_script.py
--- Scenario 1: Valid Division ---
Result of division: 5.0
Division attempt finished.
--- Scenario 2: Division by Zero ---
Error: Cannot divide by zero!
Division attempt finished.
--- Scenario 3: Valid Integer Input ---
Enter a positive number: 15
You entered: 15
--- Scenario 4: Non-Integer Input ---
Enter a positive number: abc
Invalid input: invalid literal for int() with base 10: 'abc'. Please enter an integer.
--- Scenario 5: Negative Integer Input (custom check) ---
Enter a positive number: -5
Invalid input: Number must be positive. Please enter an integer.
Multiple `except` Blocks
You can have multiple `except` blocks to handle different types of exceptions. Python tries them in order from top to bottom.
def process_data(data, divisor):
try:
value = int(data)
result = value / divisor
print(f"Processed result: {result}")
except ValueError:
print("Error: Invalid data type. Expected a number.")
except ZeroDivisionError:
print("Error: Cannot divide by zero.")
except Exception as e: # Catch-all for any other unexpected error
print(f"An unexpected error occurred: {e}")
print("\n--- Process Data Scenarios ---")
process_data("100", 5)
process_data("abc", 5)
process_data("50", 0)
process_data([1, 2], 1) # Will raise a TypeError, caught by generic Exception
--- Process Data Scenarios ---
Processed result: 20.0
Error: Invalid data type. Expected a number.
Error: Cannot divide by zero.
An unexpected error occurred: unsupported operand type(s) for /: 'list' and 'int'
The `else` Block
The `else` block runs only if no exception occurs in the `try` block. It's useful for code that should only execute after a successful `try`.
def check_file_access(filename):
try:
with open(filename, 'r') as f:
content = f.read()
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
except Exception as e:
print(f"An error occurred reading '{filename}': {e}")
else:
# This code runs ONLY if no exception occurred in the 'try' block
print(f"Successfully read from '{filename}'. First 20 chars: '{content[:20]}...'")
# Further processing can happen here
finally:
print(f"Finished attempting to access '{filename}'.")
# Create a dummy file for demonstration
with open("test_file.txt", "w") as f:
f.write("This is a test file for UdaanPath exception handling.")
print("\n--- File Access Scenarios ---")
check_file_access("test_file.txt")
check_file_access("non_existent_file.txt")
--- File Access Scenarios ---
Successfully read from 'test_file.txt'. First 20 chars: 'This is a test file '...
Finished attempting to access 'test_file.txt'.
Error: File 'non_existent_file.txt' not found.
Finished attempting to access 'non_existent_file.txt'.
The `finally` Block
The `finally` block always executes, regardless of whether an exception occurred in the `try` block, or if it was handled by an `except` block. It's perfect for cleanup operations (closing files, releasing network connections, etc.).
def process_resource(value):
f = None # Initialize file handle to None
try:
f = open("my_resource.txt", "w")
f.write(f"Processing: {100 / value}\n")
print("Write successful!")
except ZeroDivisionError:
print("Cannot process with zero.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
# This code ALWAYS runs
if f: # Check if file handle exists and is not None
f.close()
print("Resource 'my_resource.txt' closed.")
else:
print("Resource was not opened or already closed.")
print("\n--- Resource Processing Scenarios ---")
process_resource(5)
process_resource(0)
process_resource("abc") # This will raise a TypeError
--- Resource Processing Scenarios ---
Write successful!
Resource 'my_resource.txt' closed.
Cannot process with zero.
Resource was not opened or already closed.
An unexpected error occurred: unsupported operand type(s) for /: 'int' and 'str'
Resource was not opened or already closed.
Context Managers (`with` statement): For resources like files, network connections, or database handles, Python's `with` statement (which uses context managers) is generally preferred over `try-finally` for ensuring resources are properly closed. We'll cover this in a later module, but it's good to be aware that it's the more "Pythonic" way for many cleanup tasks.
3. Raising Exceptions (`raise`)
You can explicitly raise an exception in your code when a condition occurs that you consider an error. This is useful for:
-
Enforcing constraints (e.g., input must be positive).
-
Signaling an unexpected state to calling code.
-
Rethrowing an exception after partial handling.
def process_udaan_grade(grade):
if not isinstance(grade, (int, float)):
raise TypeError("Grade must be a number.")
if not 0 <= grade <= 100:
raise ValueError("Grade must be between 0 and 100.")
print(f"Grade {grade} is valid.")
print("\n--- Raising Exceptions ---")
try:
process_udaan_grade(85)
process_udaan_grade(-10) # This will raise ValueError
except ValueError as ve:
print(f"Caught an error: {ve}")
try:
process_udaan_grade("ninety") # This will raise TypeError
except TypeError as te:
print(f"Caught an error: {te}")
--- Raising Exceptions ---
Grade 85 is valid.
Caught an error: Grade must be between 0 and 100.
Caught an error: Grade must be a number.
4. Custom Exceptions
For application-specific error conditions, it's good practice to define your own custom exception classes. They typically inherit from Python's built-in `Exception` class (or a more specific built-in exception).
# Define a custom exception for UdaanPath enrollment issues
class UdaanPathEnrollmentError(Exception):
"""Custom exception for errors during UdaanPath course enrollment."""
def __init__(self, message, course_name=None):
self.message = message
self.course_name = course_name
super().__init__(self.message) # Call the base class constructor
def enroll_student(student_name, course, available_seats):
if course not in ["Python", "Web Dev", "Data Science"]:
raise UdaanPathEnrollmentError(f"Course '{course}' is not a valid UdaanPath offering.")
if available_seats[course] <= 0:
raise UdaanPathEnrollmentError(
f"No seats available for {course}.", course_name=course
)
available_seats[course] -= 1
print(f"Successfully enrolled {student_name} in {course}.")
print("\n--- Custom Exception Example ---")
udaan_seats = {"Python": 1, "Web Dev": 0, "Data Science": 5}
try:
enroll_student("Alice", "Python", udaan_seats)
enroll_student("Bob", "Web Dev", udaan_seats) # No seats
except UdaanPathEnrollmentError as e:
print(f"Enrollment failed: {e.message}")
if e.course_name:
print(f" Course affected: {e.course_name}")
try:
enroll_student("Charlie", "Blockchain", udaan_seats) # Invalid course
except UdaanPathEnrollmentError as e:
print(f"Enrollment failed: {e.message}")
--- Custom Exception Example ---
Successfully enrolled Alice in Python.
Enrollment failed: No seats available for Web Dev.
Course affected: Web Dev
Enrollment failed: Course 'Blockchain' is not a valid UdaanPath offering.