Exception Handling in Python

Exceptions are errors that occur during program execution. These errors can be caused by various factors such as invalid user input, file not found, division by zero, etc.

In Python, exceptions are events that occur during the execution of a program that disrupt the normal flow of the program’s instructions. When an error occurs, Python generates an exception that can be handled using exception handling. Exception handling ensures the program continues executing or stops gracefully without unexpected crashes. Exception handling is important because of following reasons:

  • Graceful Termination – Exception handling helps to avoid abrupt halts and unexpected crashes of the program. This allows the program to conclude gracefully (either continue executing or stops gracefully), log details, or even attempt recovery.
  • Custom Error Responses – Exception handling allows us to define how specific types of errors should be dealt with. It means we can define actions to be taken when a particular exception occurs.
  • Maintaining Program Flow – Exception handling allows us to decide how to handle errors instead of letting Python stop your program.

Exception handling – try-except block

In Python, exception handling is done using the try-except block. If any error occurs inside the try block, Python looks for an except block to handle the error.

try:
    # Code that might raise an exception
except SomeException:
    # Code to handle the exception
try:
    result = 12 / 0 # attempt to divide by zero will raise ZeroDivisionError
except ZeroDivisionError: # except block can handle the error 
    print("Cannot divide by zero!")

In this example, attempting to divide by zero raises a ZeroDivisionError, which is handled by the except block.

Multiple Exceptions Handling

We can catch multiple exceptions by specifying them in a tuple or handling them separately using multiple except blocks. Syntax of multiple exceptions handling in two ways:

  • except Error OR except Error as err – This method handles one error at a time and we can use multiple except statements to handle multiple errors. as err is used when we want to log the error or want to do some operations based on that err object.
  • except (TypeError, NameError) OR except (TypeError, NameError) as err – This method is used when we have to perform same action for multiple exceptions. Then we can use single except statement to handle multiple exceptions.
  • except – Using just except statement without defining any error, it will catch all exceptions.
try:
    num = int(input("Enter a number: "))  # Take user input from the console
    result = 12 / num
except ValueError:                        # Raise ValueError is user input is not number
    print("Invalid input! Please enter a valid number.") # Statement to execute if raised ValueError
except ZeroDivisionError:                 # Raise ZeroDivisionError is user input is zero
    print("You cannot divide by zero!")   # Statement to execute if raised ZeroDivisionError

In this example, a ValueError occurs if the user input isn’t an integer, and a ZeroDivisionError occurs if the user tries to divide by zero.

try:
	del number                  # Delete the variable number
except AttributeError as err:   # handle AttributeError
	print("error", err)         # Statement to execute if raised AttributeError
except (TypeError, NameError) as err: # handle TypeError and NameError
	print("error", err)         # Statement to execute if raised either TypeError or NameError

In this example, except (TypeError, NameError) as err handles multiple exceptions with single except statement.

Catch All Exceptions

To handle all exceptions, we can use a generic except block. However, this practice is generally discouraged because it may mask other errors unintentionally.

try:
    result = 10 / 0  # raise ZeroDivisionError
except:              # will catch all exceptions
    print("An error occurred.")

Exception handling – else block

In Python exception handling, else block is used with try-except block. else block is optional. The else block is executed only if no exceptions occur in the try block. It can be useful for defining what should happen when everything works fine. Following is the syntax for try-except-else block.

try:
    # code that might raise an exception
except SomeException:
    # code to handle the exception
else:
    # code that runs if no exceptions were raised
try:
    result = 12 / 2            # number division
except ZeroDivisionError:      # catch ZeroDivisionError
    print("Cannot divide by zero!")  # statement to execute, for ZeroDivisionError
else:                          # else block, execute when no error occurs
    print(f"Result is: {result}") # else block statement to execute 

In this example, else block statement will be executed when no ZeroDivisionError is raised.

try:
	number = 10                # declare and initialize a number
	del number                 # delete the number
except AttributeError as err:  # catch AttributeError
	print("error", err)        # statement to execute, for AttributeError
except NameError as err:       # catch NameError
	print("error", err)        # statement to execute, for NameError
else:                          # else block, execute when no error occurs
	print("else executed - only when no error is raised")

In this example, else block statement will be executed when either of AttributeError OR NameError is not raised.

Exception handling – finally block

In Python exception handling, finally block is used with try-except block. finally block is optional. The finally block is used to define code that should be executed no matter what, whether an exception occurred or not. It is often used for cleanup tasks, such as closing files or releasing resources. Following is the syntax for try-except-finally block.

try:
    # code that might raise an exception
except SomeException:
    # code to handle the exception
finally:
    # code that will run no matter what
try:
    f = open("description.txt", "r")    # open file in read and text mode
except FileNotFoundError:               # catch FileNotFoundError
    print("File not found!")            # statement to execute, for FileNotFoundError
finally:                                # finally block, execute either error raised or not
    print("This runs whether an exception occurs or not.") # finally block statement

In this example, finally block statement will be executed irrespective of FileNotFoundError is raised or not.

Nested try-catch block

In Python, we can have nested try-catch block along with else or finally block. Nested structure can be in any combinations of try-catch-else-finally blocks, like:

  • try-catch in else or finally block
  • try-catch-else in else or finally block
  • try-catch-finally in else or finally block

Example of nested try-catch block:

try:
	course_file = open("course_details.txt")
except FileNotFoundError as err:
	print("error", err)
finally:
	print("finally statement is always executed, irrespective of error raised or not")
	try:                       # try-catch block in finally block
		course_file.close()
	except NameError as err:
		print("error", err)

raise and chain of exceptions

In Python, we can raise exceptions explicitly in our code using the raise keyword. This is useful for enforcing constraints or ensuring that the program follows a certain flow. Syntax to raise exceptions:

raise SomeException("Error message")
course_list = []                # declare an empty list
try:                            # try block
	if len(course_list) <= 0:   # check if length of list is less than or equal to 0
		raise ValueError("error, no course is defined") # raise ValueError with message
except ValueError as err:       # catch ValueError
	print("error", err)         # print error

In this program, ValueError is raised if length of the list is less than or equal to zero. ValueError is catched using the except block.

Chain of exceptions

In Python, chain of exceptions is by default. Chain of exceptions happen when function_A raised an error_A and function_B catches error_A to raise error_B.

def file_operations():
	try:
		read_file()                      # call to read_file() function
	except FileNotFoundError as err:     # catch FileNotFoundError raised from read_file() function
		print("read_file error - ", err) # print error
		raise IOError("file operations failed with error") # raise IOError

def read_file():
	raise FileNotFoundError("file with given name not found") # raise FileNotFoundError

file_operations()  # call to file_operations() function

In this program, read_file() function raise FileNotFoundError which is caught in file_operations() function and then file_operations() function raise IOError.

Custom Exceptions

In Python, we can define our own exceptions by creating a class that inherits from Python’s built-in Exception class. This allows us to provide more specific and meaningful error messages for our application. Syntax to create a custom exception:

class CustomError(Exception):        # CustomErro class inherits Exception class
    def __init__(self, message):     # init function for CustomError class
        self.message = message
        super().__init__(self.message)

try:
    raise CustomError("This is a custom error.") # raise CustomError with message
except CustomError as e:                         # catch CustomError
    print(e)

Base Error Class

We can create a base Error class for Exception and then we use that class to create other exception classes. Syntax with base Error class and multiple error classes based on that base class.

# exception class in sequence from child to parent
print("exception class in sequence from child to parent")
class BaseError(Exception):       # BaseError class inherits Exception class
	print("in Parent2 class")

class ParentError(BaseError):
	print("in Parent1 class")     # ParentError class inherits BaseError class

class ChildError(ParentError):
	print("in Child class")       # ChildError class inherits ParentError class

for class_type in [ChildError, ParentError, BaseError]:  # for loop to raise all three exception
	try:
		raise class_type()
	except ChildError:                 # catch ChildError
		print("error in Child class")
	except ParentError:                # catch ParentError
		print("error in Parent class")
	except BaseError:                  # catch BaseError
		print("error in Base class")

Custom Error Class Example

class Error(Exception):                  # base Error class for Exception
	"""Base class for exceptions in this module."""
	pass

class CourseNotFoundError(Error):        # CourseNotFoundError inherits Error class
    """Exception raised when course is not found.   # Docstring for CourseNotFoundError class

    Attributes:
        name -- input name for which the exception raised
        message -- explanation of the error
    """

    def __init__(self, name, message):             # init method for CourseNotFoundError class
        self.name = name
        self.message = message

class CourseNameError(Error):           # CourseNameError inherits Error class
    """Raised when an training is not in progress. # Docstring for CourseNotFoundError class

    Attributes:
        id -- id for the training
        status -- status of training
        message -- explanation of why the training is not in progress
    """

    def __init__(self, id, name, message):        # init method for CourseNameError class
        self.id = id
        self.name = name
        self.message = message

try:
	raise(CourseNotFoundError("PHP", "Course is not started")) # raise CourseNotFoundError
except CourseNotFoundError as error:
	print("exception occur for - ", error.name, " with message - ", error.message)

try:
	raise(CourseNameError("C5", "PHP4", "Name fo course provided is wrong")) # raise CourseNameError
except CourseNameError as error:
	print("exception occur for - ", error.id, error.name, " with message - ", error.message)

Exception Handling Best Practices

  • Handle Specific Exceptions – Catch only the exceptions we expect. Avoid generic except statements.
  • Keep the Try Block Small – Only wrap code that might throw an exception in the try block, and place normal logic outside it.
  • Use finally for Cleanup – Always use the finally block for resource management, such as closing files or network connections.
  • Don’t Suppress Errors – Use exception handling to manage errors, not to hide them. Catch specific errors and log them for future debugging.

Summary

In this article, we learned about Exception Handling in Python. Following topics were discussed:

Code – Github Repository

All code snippets and programs for this article and for Python tutorial, can be accessed from Github repository – Comments and Docstring in Python.

Python Topics


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *