Python Exception Handling – Try, Except, Finally

  • Exceptions are error scenarios that alter the normal execution flow of the program.
  • The process of taking care of the possible exceptions is called exception handling.
  • If exceptions are not handled properly, the program may terminate prematurely. It can cause data corruption or unwanted results.
  • Python exception handling is achieved by three keyword blocks – try, except, and finally.
  • The try block contains the code that may raise exceptions or errors.
  • The except block is used to catch the exceptions and handle them.
  • The catch block code is executed only when the corresponding exception is raised.
  • There can be multiple catch blocks. We can also catch multiple exceptions in a single catch block.
  • The finally block code is always executed, whether the program executed properly or it raised an exception.
  • We can also create an “else” block with try-except block. The code inside the else block is executed if there are no exceptions raised.

How to Handle Exceptions in Python?

Let’s look at an example where we need exception handling.

def divide(x, y):
    print(f'{x}/{y} is {x / y}')


divide(10, 2)
divide(10, 0)
divide(10, 4)

If we run the above program, we get the following output.

10/2 is 5.0
Traceback (most recent call last):
  File "/Users/pankaj/Documents/PycharmProjects/PythonTutorialPro/hello-world/exception_handling.py", line 6, in <module>
    divide(10, 0)
  File "/Users/pankaj/Documents/PycharmProjects/PythonTutorialPro/hello-world/exception_handling.py", line 2, in divide
    print(f'{x}/{y} is {x / y}')
ZeroDivisionError: division by zero

The second call to the divide() function raised ZeroDivisionError exception and the program terminated.

We never got the output of the third call to divide() method because we didn’t do exception handling in our code.

Let’s rewrite the divide() method with proper exception handling. If someone tries to divide by 0, we will catch the exception and print an error message. This way the program will not terminate prematurely and the output will make more sense.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except ZeroDivisionError as e:
        print(e)


divide(10, 2)
divide(10, 0)
divide(10, 4)

Output:

10/2 is 5.0
division by zero
10/4 is 2.5
Python Exception Handling
Python Exception Handling

What is BaseException Class?

The BaseException class is the base class of all the exceptions. It has four sub-classes.

  1. Exception – this is the base class for all non-exit exceptions.
  2. GeneratorExit – Request that a generator exit.
  3. KeyboardInterrupt – Program interrupted by the user.
  4. SystemExit – Request to exit from the interpreter.

Some Built-In Exception Classes

Some of the built-in exception classes in Python are:

  • ArithmeticError – this is the base class for arithmetic errors.
  • AssertionError – raised when an assertion fails.
  • AttributeError – when the attribute is not found.
  • BufferError
  • EOFError – reading after end of file
  • ImportError – when the imported module is not found.
  • LookupError – base exception for lookup errors.
  • MemoryError – when out of memory occurs
  • NameError – when a name is not found globally.
  • OSError – base class for I/O errors
  • ReferenceError
  • RuntimeError
  • StopIteration, StopAsyncIteration
  • SyntaxError – invalid syntax
  • SystemError – internal error in the Python Interpreter.
  • TypeError – invalid argument type
  • ValueError – invalid argument value

Some Built-In Warning Classes

The Warning class is the base class for all the warnings. It has the following sub-classes.

  • BytesWarning – bytes, and buffer related warnings, mostly related to string conversion and comparison.
  • DeprecationWarning – warning about deprecated features
  • FutureWarning – base class for warning about constructs that will change semantically in the future.
  • ImportWarning – warning about mistakes in module imports
  • PendingDeprecationWarning – warning about features that will be deprecated in future.
  • ResourceWarning – resource usage warnings
  • RuntimeWarning – warnings about dubious runtime behavior.
  • SyntaxWarning – warning about dubious syntax
  • UnicodeWarning – Unicode conversion-related warnings
  • UserWarning – warnings generated by the user code

Handling Multiple Exceptions in a Single Except Block

A try block can have multiple except blocks. We can catch specific exceptions in each of the except blocks.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except ZeroDivisionError as e:
        print(e)
    except TypeError as e:
        print(e)
    except ValueError as e:
        print(e)

The code in every except block is the same. In this scenario, we can handle multiple exceptions in a single except block. We can pass a tuple of exception objects to an except block to catch multiple exceptions.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except (ZeroDivisionError, TypeError, ValueError) as e:
        print(e)

Catch-All Exceptions in a Single Except Block

If we don’t specify any exception class in the except block, it will catch all the exceptions raised by the try block. It’s beneficial to have this when we don’t know about the exceptions that the try block can raise.

The empty except clause must be the last one in the exception handling chain.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except ZeroDivisionError as e:
        print(e)
    except:
        print("unknown error occurred")

Using else Block with try-except

The else block code is optional. It’s executed when there are no exceptions raised by the try block.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except ZeroDivisionError as e:
        print(e)
    else:
        print("divide() function worked fine.")


divide(10, 2)
divide(10, 0)
divide(10, 4)

Output:

Python Try Except Else Block
Python Try Except Else Block

The else block code executed twice when the divide() function try block worked without any exception.


Using finally Block with try-except

The finally block code is executed in all the cases, whether there is an exception or not. The finally block is used to close resources and perform clean-up activities.

def divide(x, y):
    try:
        print(f'{x}/{y} is {x / y}')
    except ZeroDivisionError as e:
        print(e)
    else:
        print("divide() function worked fine.")
    finally:
        print("close all the resources here")


divide(10, 2)
divide(10, 0)
divide(10, 4)

Output:

Python Try Except Else Finally Block
Python Try Except Else Finally Block

Python Exception Handling Syntax

Now that we have seen everything related to exception handling in Python, the final syntax is:

try -> except 1...n -> else -> finally

We can have many except blocks for a try block. But, we can have only one else and finally block.


Creating Custom Exception Class

We can create a custom exception class by extending Exception class. The best practice is to create a base exception and then derive other exception classes. Here are some examples of creating user-defined exception classes.

class EmployeeModuleError(Exception):
    """Base Exception Class for our Employee module"""
    pass


class EmployeeNotFoundError(EmployeeModuleError):
    """Error raised when employee is not found in the database"""

    def __init__(self, emp_id, msg):
        self.employee_id = emp_id
        self.error_message = msg


class EmployeeUpdateError(EmployeeModuleError):
    """Error raised when employee update fails"""

    def __init__(self, emp_id, sql_error_code, sql_error_msg):
        self.employee_id = emp_id
        self.error_message = sql_error_msg
        self.error_code = sql_error_code

The naming convention is to suffix the name of exception class with “Error”.


Raising Exceptions

We can use raise keyword to throw an exception from our code. Some of the possible scenarios are:

  • Function input parameters validation fails
  • Catching an exception and then throwing a custom exception
class ValidationError(Exception):
    pass


def divide(x, y):
    try:
        if type(x) is not int:
            raise TypeError("Unsupported type")
        if type(y) is not int:
            raise TypeError("Unsupported type")
    except TypeError as e:
        print(e)
        raise ValidationError("Invalid type of arguments")

    if y is 0:
        raise ValidationError("We can't divide by 0.")


try:
    divide(10, 0)
except ValidationError as ve:
    print(ve)

try:
    divide(10, "5")
except ValidationError as ve:
    print(ve)

Output:

We can't divide by 0.
Unsupported type
Invalid type of arguments

Nested try-except Blocks Example

We can have nested try-except blocks in Python. In this case, if an exception is raised in the nested try block, the nested except block is used to handle it. In case the nested except is not able to handle it, the outer except blocks are used to handle the exception.

x = 10
y = 0

try:
    print("outer try block")
    try:
        print("nested try block")
        print(x / y)
    except TypeError as te:
        print("nested except block")
        print(te)
except ZeroDivisionError as ze:
    print("outer except block")
    print(ze)

Output:

outer try block
nested try block
outer except block
division by zero

Python Exception Handling Best Practices

  • Always try to handle the exception in the code to avoid abnormal termination of the program.
  • When creating a custom exception class, suffix its name with “Error”.
  • If the except clauses have the same code, try to catch multiple exceptions in a single except block.
  • Use finally block to close heavy resources and remove heavy objects.
  • Use else block to log successful execution of the code, send notifications, etc.
  • Avoid bare except clause as much as possible. If you don’t know about the exceptions, then only use it.
  • Create module-specific exception classes for specific scenarios.
  • You can catch exceptions in an except block and then raise another exception that is more meaningful.
  • Always raise exceptions with meaningful messages.
  • Avoid nested try-except blocks because it reduces the readability of the code.

References: