Python tutorials  /  Exception Handling in Python
Chapter 18 · Python

Exception Handling in Python

Exception Handling is a programming mechanism used to detect and respond to runtime errors (exceptions) in a controlled manner, using constructs like try, except, else, and finally, so that a program can gracefully handle unexpected situations instead of crashing abruptly.

Exception handling means writing code that anticipates things that could go wrong while your program runs, like dividing by zero, opening a file that doesn't exist, or converting invalid text to a number, and dealing with those problems in a controlled way instead of letting the entire program crash. Python lets you 'try' a block of risky code and 'catch' any errors that occur, so you can respond appropriately.

Think of a bank ATM. If you try to withdraw more money than your account balance, the ATM doesn't crash or shut down; instead, it detects the problem and displays a message like 'Insufficient Funds', then returns you to the main menu so you can try again. Similarly, in a Python application that reads a file uploaded by a user, if the file doesn't exist or is corrupted, exception handling allows the program to catch that specific error, show a friendly message like 'File not found, please upload again', and continue running instead of terminating unexpectedly and losing all unsaved progress.

Exception handling is needed to make programs robust and resilient to unexpected situations such as invalid user input, missing files, network failures, or division by zero. Without it, a single runtime error would crash the entire program, potentially causing data loss or a poor user experience. Exception handling allows developers to anticipate specific failure scenarios, provide meaningful error messages, perform cleanup actions like closing files or database connections, and keep the application running smoothly even when something goes wrong.

  • Built-in Exceptions: Python provides many predefined exception classes for common error scenarios, such as ZeroDivisionError, ValueError, TypeError, FileNotFoundError, IndexError, KeyError, and AttributeError, which are automatically raised when the corresponding error condition occurs.
  • try-except Block: The fundamental exception handling structure where code that might raise an error is placed inside a 'try' block, and the corresponding error-handling code is placed inside an 'except' block, which only executes if an exception occurs.
  • try-except-else-finally Block: An extended structure where the 'else' block runs only if no exception occurred in the try block, and the 'finally' block always runs regardless of whether an exception occurred, typically used for cleanup actions like closing files or releasing resources.
  • Custom (User-Defined) Exceptions: Developers can create their own exception classes by inheriting from Python's built-in Exception class, allowing them to define application-specific error types with custom messages and behavior.
  • Multiple Exception Handling: A single try block can handle multiple different types of exceptions using multiple except clauses, or by grouping exception types together in a tuple within a single except clause, allowing different error types to be handled differently or uniformly.
try: # code that might raise an exception risky_operation() except SpecificException as e: # handle a specific exception print(f"Error occurred: {e}") except (AnotherException, YetAnotherException) as e: # handle multiple exception types together print(f"Error occurred: {e}") else: # runs only if no exception occurred print("No errors occurred") finally: # always runs, used for cleanup print("Execution completed")
Suppose you are building a simple calculator application that takes two numbers as input from the user and divides them. If the user enters a non-numeric value or enters zero as the divisor, your program would normally crash with an unhandled exception, showing a confusing traceback message and terminating abruptly. You need a way to detect these specific error conditions, show the user a friendly and helpful error message, and allow them to try again without restarting the entire program. Exception handling solves this by letting you catch ValueError for invalid numeric input and ZeroDivisionError for division by zero, handling each case appropriately.
Basic Exception Handling with try-except
Demonstrates catching a ZeroDivisionError to prevent the program from crashing when dividing by zero.
Python
def divide_numbers(a, b): try: result = a / b print(f"Result: {result}") except ZeroDivisionError: print("Error: Cannot divide by zero") divide_numbers(10, 2) divide_numbers(10, 0)
Result: 5.0 Error: Cannot divide by zero
The first call successfully divides 10 by 2 and prints the result. The second call attempts to divide by zero, which raises a ZeroDivisionError, but instead of crashing, the except block catches the error and prints a friendly message.
Handling Multiple Exceptions with else and finally
Demonstrates a complete try-except-else-finally structure that handles multiple exception types and always performs cleanup.
Python
def process_input(value): try: number = int(value) result = 100 / number except ValueError: print("Error: Please enter a valid number") except ZeroDivisionError: print("Error: Cannot divide by zero") else: print(f"Success! Result: {result}") finally: print("Processing complete\n") process_input("5") process_input("abc") process_input("0")
Success! Result: 20.0 Processing complete Error: Please enter a valid number Processing complete Error: Cannot divide by zero Processing complete
For '5', no exception occurs, so the else block runs and prints the successful result. For 'abc', a ValueError is caught since it cannot be converted to an integer. For '0', a ZeroDivisionError is caught. In all three cases, the finally block executes at the end, demonstrating that cleanup code always runs regardless of the outcome.
Creating and Raising Custom Exceptions
Shows how to define a custom exception class for application-specific error handling, such as validating a withdrawal amount in a banking system.
Python
class InsufficientFundsError(Exception): def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f"Cannot withdraw {amount}. Available balance: {balance}") def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(balance, amount) return balance - amount try: new_balance = withdraw(1000, 1500) except InsufficientFundsError as e: print(f"Transaction failed: {e}")
Transaction failed: Cannot withdraw 1500. Available balance: 1000
InsufficientFundsError is a custom exception class inheriting from Exception, with a custom message format. When the withdrawal amount exceeds the balance, this exception is explicitly raised using the 'raise' keyword and caught in the except block, allowing for application-specific error handling.
Using finally for Resource Cleanup
Demonstrates using the finally block to ensure a file is properly closed even if an error occurs while processing it.
Python
def read_file(filename): file = None try: file = open(filename, 'r') content = file.read() print(content) except FileNotFoundError: print(f"Error: {filename} not found") finally: if file: file.close() print("File closed successfully") else: print("No file was opened") read_file("non_existent_file.txt")
Error: non_existent_file.txt not found No file was opened
Since the file does not exist, opening it raises a FileNotFoundError, which is caught by the except block. The finally block always executes afterward, checking whether the file was successfully opened before attempting to close it, demonstrating safe resource cleanup regardless of success or failure.
  • Using a Bare except Clause: Writing 'except:' without specifying an exception type catches all exceptions, including system-exiting exceptions like KeyboardInterrupt and SystemExit, which can hide bugs, make debugging difficult, and prevent the program from being stopped normally with Ctrl+C. It is better to catch specific exception types or at least use 'except Exception' instead.
  • Catching Exceptions Too Broadly and Silently Ignoring Them: Wrapping large blocks of code in a single try-except and catching generic exceptions without logging or handling them properly (e.g., using 'except Exception: pass') hides real bugs and makes it very difficult to diagnose issues later, since errors disappear silently instead of being reported.
  • Not Using finally for Critical Cleanup Operations: Forgetting to use a finally block (or a context manager with 'with') for closing files, releasing database connections, or unlocking resources can lead to resource leaks if an exception occurs after the resource was opened but before it was properly closed.
  • Raising Generic Exceptions Instead of Specific or Custom Ones: Using 'raise Exception("Something went wrong")' for all error conditions makes it impossible for calling code to catch and handle specific error types differently. Defining and raising specific custom exception classes provides clearer, more actionable error handling.
  • Placing Too Much Code Inside the try Block: Including large amounts of unrelated code inside a single try block makes it unclear which specific line caused an exception and increases the chance of accidentally catching and mishandling an unrelated error. It's better to keep try blocks focused on the specific risky operation.
  • Catch Specific Exceptions Rather Than Generic Ones: Always catch the most specific exception type possible (e.g., ValueError, FileNotFoundError) instead of a generic Exception or bare except, so that your code only handles the errors it actually expects and unexpected errors are not silently swallowed.
  • Use finally or Context Managers for Resource Cleanup: Use a finally block, or better yet, Python's 'with' statement (context managers), when working with files, database connections, or network resources, to guarantee that cleanup code runs even if an exception occurs.
  • Create Custom Exceptions for Application-Specific Errors: Define custom exception classes that inherit from Exception for domain-specific error conditions in your application (like InsufficientFundsError or InvalidUserInputError), making your code more readable and allowing callers to handle different error types distinctly.
  • Log Exceptions Instead of Silently Ignoring Them: When catching an exception, always log the error details (using the 'logging' module) or at least print meaningful information, rather than using a silent 'pass', so that issues can be diagnosed and traced later during debugging or in production.
  • Keep try Blocks Small and Focused: Limit the code inside a try block to only the specific operation that might raise an exception, rather than wrapping large sections of unrelated code, making it clear exactly which operation is being protected and easier to understand which exception corresponds to which line.
What is the difference between an error and an exception in Python?
An error generally refers to a serious problem that a program usually cannot recover from, such as a SyntaxError that prevents the code from even running. An exception is a runtime event that disrupts the normal flow of a program's execution but can potentially be caught and handled using try-except blocks, allowing the program to continue running, such as ZeroDivisionError or FileNotFoundError.
Explain the purpose of the try, except, else, and finally blocks in Python.
The 'try' block contains code that might raise an exception. The 'except' block catches and handles a specific exception if it occurs. The 'else' block runs only if no exception occurred in the try block, useful for code that should execute only on success. The 'finally' block always executes, regardless of whether an exception occurred, and is typically used for cleanup actions like closing files or releasing resources.
What is the difference between using a bare 'except:' and 'except Exception as e:'?
A bare 'except:' catches all exceptions, including system-level exceptions like KeyboardInterrupt and SystemExit, which can prevent the program from being interrupted normally and can hide critical bugs. Using 'except Exception as e:' catches all standard exceptions derived from the Exception class but excludes system-exiting exceptions, making it a safer and more common choice, while still allowing access to the exception object via 'e' for logging or inspection.
How do you create and raise a custom exception in Python?
You create a custom exception by defining a new class that inherits from Python's built-in Exception class (or a more specific built-in exception). You can override the __init__ method to accept custom parameters and pass a custom message to the parent class using super().__init__(). You raise the custom exception using the 'raise' keyword followed by an instance of the exception class, optionally with arguments.
What is exception chaining in Python, and how is it triggered?
Exception chaining occurs when a new exception is raised while handling another exception, and Python automatically links them together, showing both the original exception and the new one in the traceback (using the phrase 'During handling of the above exception, another exception occurred'). It can also be done explicitly using the 'raise NewException() from original_exception' syntax, which is useful for providing more context about why a secondary error occurred.
What is the difference between raising an exception and using assert statements for error handling?
The 'raise' statement explicitly raises a specific exception and is used for handling expected runtime error conditions in production code, such as invalid user input or failed operations. The 'assert' statement is primarily intended for debugging and internal testing, checking that a condition is true and raising an AssertionError if it's not; assertions can be globally disabled when Python is run with optimization flags (-O), so they should not be relied upon for critical error handling in production code.
Write a function that takes a list and an index as input, and safely returns the element at that index. Use exception handling to catch an IndexError and print a friendly error message if the index is out of range.
def get_element(lst, index): try: return lst[index] except IndexError: print(f"Error: Index {index} is out of range for the list") return None numbers = [10, 20, 30] print(get_element(numbers, 1)) print(get_element(numbers, 10))
Create a custom exception called 'NegativeAgeError' that is raised when a function receives a negative value for age. Write a function 'validate_age(age)' that raises this exception for negative ages and prints a success message otherwise.
class NegativeAgeError(Exception): def __init__(self, age): self.age = age super().__init__(f"Invalid age: {age}. Age cannot be negative") def validate_age(age): if age < 0: raise NegativeAgeError(age) print(f"Age {age} is valid") try: validate_age(25) validate_age(-5) except NegativeAgeError as e: print(f"Error: {e}")
Write a program that opens a file, reads its contents, and processes each line by converting it to an integer and summing all values. Handle FileNotFoundError, ValueError, and use a finally block to indicate when processing is complete, regardless of success or failure.
def sum_numbers_from_file(filename): total = 0 try: file = open(filename, 'r') for line in file: total += int(line.strip()) file.close() except FileNotFoundError: print(f"Error: {filename} not found") return None except ValueError: print("Error: File contains non-numeric data") return None finally: print("File processing attempt completed") return total result = sum_numbers_from_file("numbers.txt") if result is not None: print(f"Total sum: {result}")

Exception Handling in Python is a mechanism for detecting and responding to runtime errors using try, except, else, and finally blocks, allowing programs to handle unexpected situations gracefully instead of crashing. Python provides many built-in exception types for common error scenarios, and developers can create custom exception classes for application-specific error handling. Following best practices like catching specific exceptions, using finally blocks or context managers for cleanup, and avoiding bare except clauses leads to more robust, maintainable, and debuggable code.

© 2026 CompileX. Maintained by Aditya Kumar Sharma.