Module 14: Exception Handling in Python
Errors are a normal part of software development. A user may enter invalid input, a file may not exist, a network connection may fail, or a program may receive data in an unexpected format.
If these situations are not handled properly, a Python application can terminate unexpectedly.
Exception handling allows a program to detect runtime problems and respond appropriately instead of crashing without a useful explanation.
Python provides several keywords for exception handling:
tryexceptelsefinallyraise
Python also allows developers to create custom exception classes for application-specific errors.
By the end of this module, you will understand how to:
- Identify exceptions
- Handle exceptions with
tryandexcept - Catch specific exception types
- Use
elseandfinally - Access exception messages
- Raise exceptions manually
- Create custom exceptions
- Chain exceptions
- Handle multiple exceptions
- Build reliable Python applications
What Is an Exception?
An exception is an event that occurs during program execution and interrupts the normal flow of the program.
For example:
1number = int(input("Enter a number: ")) 2 3result = 100 / number 4 5print(result)
If the user enters:
10
Python raises:
1ZeroDivisionError: division by zero
If the user enters:
1hello
Python raises a ValueError because "hello" cannot be converted into an integer.
Without exception handling, the program stops when the exception is not handled.
Why Use Exception Handling?
Consider this program:
1number = int(input("Enter number: ")) 2 3print(100 / number) 4 5print("Program finished")
If the user enters 0, the program raises an exception before reaching:
1print("Program finished")
Exception handling allows us to handle the problem:
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except ZeroDivisionError: 6 print("Cannot divide by zero.") 7 8except ValueError: 9 print("Please enter a valid integer.") 10 11else: 12 print("Result:", result) 13 14print("Program finished")
If the user enters:
10
the program can display:
1Cannot divide by zero. 2Program finished
The application can continue in a controlled way.
Exception vs Syntax Error
It is important to distinguish between syntax errors and exceptions.
A syntax error occurs when Python cannot understand the structure of your code.
Example:
1if True 2 print("Hello")
Python reports a syntax error because the colon is missing.
An exception generally occurs while valid Python code is executing.
Example:
1print(10 / 0)
This produces:
1ZeroDivisionError
In simple terms:
1Syntax Error → Python cannot parse the code 2 3Exception → Code was parsed, but something went wrong during execution
Common Python Exceptions
Python provides many built-in exception types.
| Exception | Common Cause |
|---|---|
ValueError | Invalid value |
TypeError | Incompatible data types |
ZeroDivisionError | Division by zero |
IndexError | Invalid sequence index |
KeyError | Missing dictionary key |
NameError | Undefined variable |
AttributeError | Missing object attribute |
FileNotFoundError | File does not exist |
PermissionError | Insufficient permissions |
ModuleNotFoundError | Module cannot be found |
ImportError | Import operation fails |
OSError | Operating-system-related error |
OverflowError | Numeric result exceeds supported range |
Exception Handling Flow
The basic flow is:
1try 2 │ 3 ├── No Exception ──→ else 4 │ 5 └── Exception ─────→ except 6 │ 7 ↓ 8 finally
The finally block runs whether an exception occurs or not.
The try Statement
The try block contains code that may raise an exception.
Basic syntax:
1try: 2 # Code that may fail
A try statement must be followed by at least one except or a finally clause.
Example:
1try: 2 number = int(input("Enter number: ")) 3 print(number) 4 5except ValueError: 6 print("Invalid input.")
If the user enters:
125
the output is:
125
If the user enters:
1hello
the exception is handled:
1Invalid input.
The except Statement
The except block defines how the program should respond when a particular exception occurs.
Example:
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except ZeroDivisionError: 6 print("Cannot divide by zero.") 7 8except ValueError: 9 print("Please enter a valid number.")
There are two possible errors:
ValueErrorwhen conversion to an integer failsZeroDivisionErrorwhen the user enters0
Handling them separately allows the program to provide a more useful message.
Catch Specific Exceptions
Avoid catching every possible exception when you know which errors you expect.
Instead of:
1try: 2 number = int(input("Enter number: ")) 3 print(100 / number) 4 5except: 6 print("Something went wrong.")
prefer:
1try: 2 number = int(input("Enter number: ")) 3 print(100 / number) 4 5except ValueError: 6 print("Please enter a valid integer.") 7 8except ZeroDivisionError: 9 print("Cannot divide by zero.")
Specific exception handling makes programs easier to understand and debug.
Handling Multiple Exceptions
If several exceptions should receive the same response, they can be grouped into a tuple:
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except (ValueError, ZeroDivisionError): 6 print("Invalid calculation.")
This is useful when the handling logic is identical.
If different errors need different messages, separate except blocks are clearer.
Accessing the Exception Object
You can store the exception object using as.
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except (ValueError, ZeroDivisionError) as error: 6 print("Error:", error)
For example, entering 0 may produce:
1Error: division by zero
The exception object contains information about what went wrong.
The else Block
The else block runs only when the try block completes successfully.
Syntax:
1try: 2 # Risky operation 3 4except SomeError: 5 # Handle error 6 7else: 8 # Runs when no exception occurs
Example:
1try: 2 number = int(input("Enter number: ")) 3 4except ValueError: 5 print("Please enter a valid integer.") 6 7else: 8 print("You entered:", number)
If the user enters:
125
the output is:
1You entered: 25
The else block is useful for code that should run only after the operation inside try succeeds.
The finally Block
The finally block always executes.
It runs whether an exception occurs or not.
Example:
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except ZeroDivisionError: 6 print("Cannot divide by zero.") 7 8finally: 9 print("Calculation finished.")
If the user enters 0:
1Cannot divide by zero. 2Calculation finished.
If the user enters 20:
1Calculation finished.
The finally block is commonly used for cleanup operations.
When to Use finally
finally is useful when a resource must be cleaned up regardless of success or failure.
Examples include:
- Closing a database connection
- Releasing a resource
- Cleaning temporary state
- Closing manually managed files
- Releasing locks
For ordinary file operations, however, Python's with statement is usually preferable.
Instead of:
1file = None 2 3try: 4 file = open("notes.txt", encoding="utf-8") 5 print(file.read()) 6 7finally: 8 if file is not None: 9 file.close()
prefer:
1with open("notes.txt", encoding="utf-8") as file: 2 print(file.read())
The context manager handles file cleanup automatically.
Complete try-except-else-finally Example
1try: 2 number = int(input("Enter number: ")) 3 result = 100 / number 4 5except ValueError: 6 print("Please enter a valid integer.") 7 8except ZeroDivisionError: 9 print("Cannot divide by zero.") 10 11else: 12 print("Result:", result) 13 14finally: 15 print("Execution completed.")
Possible successful output:
1Enter number: 25 2Result: 4.0 3Execution completed.
Possible error output:
1Enter number: 0 2Cannot divide by zero. 3Execution completed.
The raise Statement
The raise statement allows you to manually generate an exception.
Syntax:
1raise ExceptionType("Error message")
Example:
1age = int(input("Enter age: ")) 2 3if age < 18: 4 raise ValueError("Age must be at least 18.") 5 6print("Access granted.")
If the user enters 15, Python raises:
1ValueError: Age must be at least 18.
raise is useful when your program detects invalid application-specific data.
Validating User Input With raise
Consider a salary validation example:
1salary = float(input("Enter salary: ")) 2 3if salary < 0: 4 raise ValueError("Salary cannot be negative.") 5 6print("Salary accepted:", salary)
The input conversion handles values that are not numbers, while raise handles the business rule that salary cannot be negative.
A more complete version is:
1try: 2 salary = float(input("Enter salary: ")) 3 4 if salary < 0: 5 raise ValueError("Salary cannot be negative.") 6 7except ValueError as error: 8 print("Invalid salary:", error) 9 10else: 11 print("Salary accepted:", salary)
Re-Raising an Exception
Sometimes you want to log or inspect an exception and then allow it to propagate.
You can use raise without an argument inside an except block:
1try: 2 result = 10 / 0 3 4except ZeroDivisionError as error: 5 print("Logging error:", error) 6 raise
The original exception is re-raised after the message is printed.
This is useful when a lower-level function should not silently hide an error from its caller.
Custom Exceptions
Python allows you to create custom exception classes.
Basic syntax:
1class MyException(Exception): 2 pass
A custom exception should normally inherit from Exception or one of its subclasses.
Example:
1class InvalidAgeError(Exception): 2 pass
Now you can raise it:
1age = int(input("Enter age: ")) 2 3if age < 18: 4 raise InvalidAgeError("You must be at least 18 years old.") 5 6print("Access granted.")
Custom exceptions are useful when built-in exception types do not clearly describe a domain-specific problem.
Handling a Custom Exception
1class NegativeNumberError(Exception): 2 pass 3 4 5try: 6 number = int(input("Enter number: ")) 7 8 if number < 0: 9 raise NegativeNumberError( 10 "Negative numbers are not allowed." 11 ) 12 13 print("Number:", number) 14 15except NegativeNumberError as error: 16 print("Error:", error)
If the user enters:
1-5
the output is:
1Error: Negative numbers are not allowed.
Custom Exceptions With More Information
A custom exception can store additional information.
1class InsufficientBalanceError(Exception): 2 def __init__(self, balance, amount): 3 self.balance = balance 4 self.amount = amount 5 6 super().__init__( 7 f"Balance {balance} is insufficient for {amount}." 8 )
Use it like this:
1def withdraw(balance, amount): 2 if amount > balance: 3 raise InsufficientBalanceError(balance, amount) 4 5 return balance - amount 6 7 8try: 9 remaining = withdraw(500, 800) 10 print("Remaining balance:", remaining) 11 12except InsufficientBalanceError as error: 13 print(error)
Output:
1Balance 500 is insufficient for 800.
This approach is useful in larger applications where callers may need structured information about an error.
Exception Chaining
Sometimes one exception occurs because another exception happened first.
Python supports exception chaining with from.
Example:
1def get_number(): 2 try: 3 return int("abc") 4 except ValueError as error: 5 raise RuntimeError("Could not load the number.") from error
Calling:
1get_number()
produces an error showing that the RuntimeError was caused by the original ValueError.
This preserves the original cause and makes debugging easier.
Nested Exception Handling
Exception handlers can contain other exception-handling blocks.
Example:
1try: 2 try: 3 result = 10 / 0 4 5 except ZeroDivisionError: 6 print("Inner exception handled.") 7 8except Exception: 9 print("Outer exception handled.")
Output:
1Inner exception handled.
The inner except handles the ZeroDivisionError, so the outer handler does not need to handle it.
Nested exception handling should be used only when the program's structure actually requires it. Excessive nesting can make code difficult to follow.
Exception Hierarchy
Python exceptions are organized into a hierarchy.
For example:
1BaseException 2 └── Exception 3 ├── ValueError 4 ├── TypeError 5 ├── OSError 6 │ ├── FileNotFoundError 7 │ └── PermissionError 8 └── RuntimeError
This means you can catch a broad parent exception:
1except OSError: 2 ...
which can handle certain subclasses such as:
1FileNotFoundError 2PermissionError
However, catch the most specific exception that you can reasonably handle.
The Order of except Blocks Matters
When handling related exceptions, put more specific exceptions before broader ones.
Correct:
1try: 2 with open("data.txt", encoding="utf-8") as file: 3 print(file.read()) 4 5except FileNotFoundError: 6 print("File not found.") 7 8except OSError: 9 print("Another file-system error occurred.")
FileNotFoundError is more specific than OSError.
Avoid putting a broad handler first:
1try: 2 ... 3except OSError: 4 ... 5except FileNotFoundError: 6 ...
The second handler would never be reached for FileNotFoundError because that exception is already caught by OSError.
Avoid Bare except
This works:
1try: 2 print(10 / 0) 3 4except: 5 print("Error")
But it is usually a poor practice.
A bare except can catch unexpected exceptions and make debugging difficult.
Prefer:
1try: 2 print(10 / 0) 3 4except ZeroDivisionError: 5 print("Cannot divide by zero.")
If you intentionally want a broad handler, use:
1except Exception as error: 2 print("Unexpected error:", error)
Even then, avoid using broad exception handling when a more specific exception can be handled.
Avoid Silently Ignoring Exceptions
This is dangerous:
1try: 2 process_data() 3 4except Exception: 5 pass
The program hides the problem, making failures difficult to diagnose.
Instead:
1try: 2 process_data() 3 4except Exception as error: 5 print("Processing failed:", error)
In production applications, logging is generally preferable to simply printing errors.
Exception Handling Should Be Targeted
Do not put an entire application inside one huge try block.
Avoid:
1try: 2 get_user() 3 validate_user() 4 connect_database() 5 process_payment() 6 save_data() 7 8except Exception: 9 print("Something went wrong.")
This makes it difficult to determine which operation failed.
Instead, keep try blocks focused:
1try: 2 user_id = int(input("User ID: ")) 3except ValueError: 4 print("Invalid user ID.") 5 return 6 7try: 8 user = get_user(user_id) 9except UserNotFoundError: 10 print("User not found.") 11 return
Small, focused try blocks make exception handling more predictable.
Practice Project: Safe Calculator
Let's build a calculator that handles:
- Invalid numbers
- Invalid operators
- Division by zero
1def calculate(first, second, operator): 2 if operator == "+": 3 return first + second 4 5 if operator == "-": 6 return first - second 7 8 if operator == "*": 9 return first * second 10 11 if operator == "/": 12 if second == 0: 13 raise ZeroDivisionError( 14 "Cannot divide by zero." 15 ) 16 17 return first / second 18 19 raise ValueError( 20 f"Unsupported operator: {operator}" 21 ) 22 23 24try: 25 first = float(input("First number: ")) 26 second = float(input("Second number: ")) 27 operator = input("Operator (+, -, *, /): ").strip() 28 29 result = calculate(first, second, operator) 30 31except ValueError as error: 32 print("Input error:", error) 33 34except ZeroDivisionError as error: 35 print("Calculation error:", error) 36 37else: 38 print("Result:", result) 39 40finally: 41 print("Calculator closed.")
Example:
1First number: 25 2Second number: 0 3Operator (+, -, *, /): / 4 5Calculation error: Cannot divide by zero. 6Calculator closed.
Separating the calculation into a function makes the program easier to test and reuse.
Practice Project: Safe File Reader
A file reader should handle cases where the file does not exist or cannot be accessed.
1from pathlib import Path 2 3 4def read_file(filename): 5 path = Path(filename) 6 7 try: 8 return path.read_text(encoding="utf-8") 9 10 except FileNotFoundError: 11 print("File not found.") 12 return None 13 14 except PermissionError: 15 print("Permission denied.") 16 return None 17 18 19content = read_file("notes.txt") 20 21if content is not None: 22 print(content)
This example demonstrates exception handling together with pathlib.
Practice Project: Password Validator
A custom exception can make validation errors easier to understand.
1class WeakPasswordError(Exception): 2 pass 3 4 5def validate_password(password): 6 if len(password) < 8: 7 raise WeakPasswordError( 8 "Password must contain at least 8 characters." 9 ) 10 11 if password.isalpha(): 12 raise WeakPasswordError( 13 "Password should contain a number or symbol." 14 ) 15 16 return True 17 18 19try: 20 password = input("Enter password: ") 21 22 validate_password(password) 23 24except WeakPasswordError as error: 25 print("Invalid password:", error) 26 27else: 28 print("Password accepted.")
For real authentication systems, password validation should follow the application's security requirements rather than relying only on simple rules like this example.
Practice Project: Bank Withdrawal
1class InsufficientBalanceError(Exception): 2 pass 3 4 5def withdraw(balance, amount): 6 if amount <= 0: 7 raise ValueError("Withdrawal must be greater than zero.") 8 9 if amount > balance: 10 raise InsufficientBalanceError( 11 "Insufficient account balance." 12 ) 13 14 return balance - amount 15 16 17try: 18 balance = 5000 19 amount = float(input("Withdrawal amount: ")) 20 21 balance = withdraw(balance, amount) 22 23except ValueError as error: 24 print("Invalid amount:", error) 25 26except InsufficientBalanceError as error: 27 print("Transaction failed:", error) 28 29else: 30 print("Withdrawal successful.") 31 print("Remaining balance:", balance)
This example demonstrates how custom exceptions can represent application-specific business rules.
Exception Handling With Functions
Exception handling can happen inside a function:
1def divide(a, b): 2 try: 3 return a / b 4 except ZeroDivisionError: 5 return None 6 7 8result = divide(10, 0) 9 10if result is None: 11 print("Division failed.")
However, sometimes it is better for the function to let the exception propagate to the caller:
1def divide(a, b): 2 return a / b 3 4 5try: 6 result = divide(10, 0) 7except ZeroDivisionError: 8 print("Cannot divide by zero.")
Which approach is better depends on the responsibility of the function.
A useful principle is:
Handle an exception where you have enough information to recover from it.
Common Mistakes
Using a Bare except
Avoid:
1try: 2 risky_operation() 3except: 4 print("Error")
Prefer a specific exception:
1try: 2 risky_operation() 3except ValueError: 4 print("Invalid value.")
Catching Exception Too Early
Avoid:
1try: 2 process_data() 3except Exception: 4 print("Failed")
unless you genuinely need a broad fallback.
Specific handlers are usually better:
1try: 2 process_data() 3except ValueError: 4 print("Invalid data.") 5except FileNotFoundError: 6 print("Required file is missing.")
Silently Ignoring Errors
Avoid:
1try: 2 process_data() 3except Exception: 4 pass
This hides failures.
Instead:
1try: 2 process_data() 3except Exception as error: 4 print("Processing failed:", error)
For production applications, use an appropriate logging system.
Putting Too Much Code Inside try
Avoid:
1try: 2 # Hundreds of lines 3 ... 4except Exception: 5 ...
Keep the try block focused on operations that can actually raise the exception you intend to handle.
Using finally When with Is Better
Instead of manually managing a file:
1file = open("data.txt", encoding="utf-8") 2 3try: 4 print(file.read()) 5finally: 6 file.close()
prefer:
1with open("data.txt", encoding="utf-8") as file: 2 print(file.read())
The context manager is clearer and less error-prone.
Additional Practice Exercises
Exercise 1: Safe List Access
Given:
1numbers = [10, 20, 30]
Ask the user for an index and safely handle:
1ValueError 2IndexError
Exercise 2: Dictionary Lookup
Given:
1student = { 2 "name": "Ankit", 3 "age": 22, 4}
Ask the user for a key and handle KeyError.
Exercise 3: Safe File Reader
Create a program that reads data.txt.
Handle:
1FileNotFoundError 2PermissionError
Exercise 4: Integer Validator
Create a function:
1def get_integer(): 2 ...
The function should repeatedly ask the user for an integer until valid input is provided.
Exercise 5: Password Validator
Create a custom exception called:
1WeakPasswordError
Raise it when a password is shorter than eight characters.
Exercise 6: Bank Account
Create:
1InsufficientBalanceError
Raise it when a withdrawal amount is greater than the account balance.
Exercise 7: Safe Calculator
Build a calculator that handles:
- Invalid numbers
- Invalid operators
- Division by zero
Use separate exception handlers for different errors.
Quick Exception Handling Reference
| Keyword | Purpose |
|---|---|
try | Contains code that may raise an exception |
except | Handles an exception |
else | Runs when no exception occurs |
finally | Runs whether an exception occurs or not |
raise | Manually raises an exception |
Basic structure:
1try: 2 risky_operation() 3 4except SpecificError as error: 5 handle_error(error) 6 7else: 8 handle_success() 9 10finally: 11 cleanup()
Key Takeaways
After completing this module, you should understand:
- Exceptions are runtime events that can interrupt normal program execution.
trycontains code that may raise an exception.excepthandles exceptions.- Specific exceptions should generally be caught instead of using a bare
except. - Multiple exceptions can be handled separately or as a tuple.
elseruns only when thetryblock succeeds.finallyruns regardless of whether an exception occurs.raiseallows you to generate exceptions manually.- Custom exceptions can represent application-specific errors.
- Exception chaining with
raise ... from ...preserves the original cause. - Exception handling should be focused and targeted.
- Avoid silently ignoring exceptions.
- Use
withfor resources such as files when an appropriate context manager exists. - Good exception handling makes applications more reliable, maintainable, and easier to debug.
Exception handling is an essential Python skill. Once you understand it, you can build applications that handle invalid input, missing files, failed operations, and business-rule violations in a controlled and user-friendly way.