Creating Custom Python Exception Types

45 minutes
  • 3 Learning Objectives

About this Hands-on Lab

When building a larger system with custom classes, we will likely have different situations come up that wouldn’t be encompassed by existing exceptions. In this hands-on lab, we’ll create a few custom exception types that will fit into our employee management class hierarchy. To feel comfortable completing this lab, you’ll want to know how to create custom exception types (watch the “Creating Custom Exception Types” video from the [Certified Associate in Python Programming Certification](https://linuxacademy.com/cp/modules/view/id/470) course).

Learning Objectives

Successfully complete this lab by achieving the following learning objectives:

Create `MissingEmployeeError` and `DatabaseError` in `employee.py`

Our custom errors don’t need to have any implementation, so we’re going to create them at the top of our employee.py file to inherit from Exception with pass as the class body.

~/employee.py

class MissingEmployeeError(Exception):
    pass

class DatabaseError(Exception):
    pass

# Rest of file unchanged and omitted

Now we’ll need to use these exception types in the proper areas of the Employee class.

Raise `DatabaseError` Anywhere We Fail to Open the Database File

Our DatabaseError occurs whenever we can’t open and read the database. This can happen in two situations:

  • A FileNotFoundError because the file doesn’t exist yet
  • A PermissionError because we’re not allowed to access the file

Everywhere we use open in the Employee class, we need to catch these exceptions and instead raise a DatabaseError.

~/employee.py

# custom errors omitted

class Employee:
    default_db_file = "employee_file.txt"

    @classmethod
    def get_all(cls, file_name=None):
        results = []

        if not file_name:
            file_name = cls.default_db_file

        try:
            with open(file_name, "r") as f:
                lines = [
                    line.strip("n").split(",") + [index + 1]
                    for index, line in enumerate(f.readlines())
                ]
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))

        for line in lines:
            results.append(cls(*line))

        return results

    @classmethod
    def get_at_line(cls, line_number, file_name=None):
        if not file_name:
            file_name = cls.default_db_file

        try:
            with open(file_name, "r") as f:
                line = [
                    line.strip("n").split(",") + [index + 1]
                    for index, line in enumerate(f.readlines())
                ][line_number - 1]
                return cls(*line)
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))

    def __init__(self, name, email_address, title, phone_number=None, identifier=None):
        self.name = name
        self.email_address = email_address
        self.title = title
        self.phone_number = phone_number
        self.identifier = identifier

    def email_signature(self, include_phone=False):
        signature = f"{self.name} - {self.title}n{self.email_address}"
        if include_phone and self.phone_number:
            signature += f" ({self.phone_number})"
        return signature

    def save(self, file_name=None):
        if not file_name:
            file_name = self.default_db_file

        try:
            with open(file_name, "r+") as f:
                lines = f.readlines()
                if self.identifier:
                    lines[self.identifier - 1] = self._database_line()
                else:
                    lines.append(self._database_line())
                f.seek(0)
                f.writelines(lines)
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))

    def _database_line(self):
        return (
            ",".join(
                [self.name, self.email_address, self.title, self.phone_number or ""]
            )
            + "n"
        )

Now all the areas where our class interacts with the database file will raise the proper error if accessing the file doesn’t go according to plan.

Raise `MissingEmployeeError` in `get_at_line` and `save` if `IndexError` Occurs

Within get_at_line and save, we need to handle the potential IndexError that will be raised if we try to access a line that doesn’t exist. This indicates the employee we were searching for wasn’t found, so we want to raise the MissingEmployeeError.

~/employee.py

# custom errors omitted

class Employee:
    default_db_file = "employee_file.txt"

    @classmethod
    def get_all(cls, file_name=None):
        results = []

        if not file_name:
            file_name = cls.default_db_file

        try:
            with open(file_name, "r") as f:
                lines = [
                    line.strip("n").split(",") + [index + 1]
                    for index, line in enumerate(f.readlines())
                ]
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))

        for line in lines:
            results.append(cls(*line))

        return results

    @classmethod
    def get_at_line(cls, line_number, file_name=None):
        if not file_name:
            file_name = cls.default_db_file

        try:
            with open(file_name, "r") as f:
                line = [
                    line.strip("n").split(",") + [index + 1]
                    for index, line in enumerate(f.readlines())
                ][line_number - 1]
                return cls(*line)
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))
        except IndexError:
            raise MissingEmployeeError(f"no employee at line {line_number}")

    def __init__(self, name, email_address, title, phone_number=None, identifier=None):
        self.name = name
        self.email_address = email_address
        self.title = title
        self.phone_number = phone_number
        self.identifier = identifier

    def email_signature(self, include_phone=False):
        signature = f"{self.name} - {self.title}n{self.email_address}"
        if include_phone and self.phone_number:
            signature += f" ({self.phone_number})"
        return signature

    def save(self, file_name=None):
        if not file_name:
            file_name = self.default_db_file

        try:
            with open(file_name, "r+") as f:
                lines = f.readlines()
                if self.identifier:
                    lines[self.identifier - 1] = self._database_line()
                else:
                    lines.append(self._database_line())
                f.seek(0)
                f.writelines(lines)
        except (FileNotFoundError, PermissionError) as err:
            raise DatabaseError(str(err))
        except IndexError:
            raise MissingEmployeeError(f"no employee at line {self.identifier}")

    def _database_line(self):
        return (
            ",".join(
                [self.name, self.email_address, self.title, self.phone_number or ""]
            )
            + "n"
        )

Now if we run the test_custom_exceptions.py script, we shouldn’t see any issues:

python3.7 test_custom_exceptions.py

Additional Resources

The main issues we can run into with our Employee code are around reading and writing to the "database" file this class uses to persist information in the get_all, get_at_line, and save methods. The errors we need are MissingEmployeeError and DatabaseError. We'll do MissingEmployeeError when we try to either get_at_line with a line that isn't in the file or when we try to save an instance when its identifier doesn't exist in the file. The DatabaseError will happen when we can't access the designated database file.

Logging In

There are a couple ways to get in and work with the code. One is to use the credentials provided in the hands-on lab overview page, log in with SSH, and use a text editor in the terminal.

The other is using VS Code in the browser. If you'd like to go this route, then you will need to navigate to the public IP address of the workstation server (provided in the hands-on lab overview page) on port 8080 (example: http://<PUBLIC_IP>:8080). Your password will be the same password that you'd use to connect over SSH.

What are Hands-on Labs

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Get Started
Who’s going to be learning?

How many seats do you need?

  • $499 USD per seat per year
  • Billed Annually
  • Renews in 12 months

Ready to accelerate learning?

For over 25 licenses, a member of our sales team will walk you through a custom tailored solution for your business.


$2,495.00

Checkout
Sign In
Welcome Back!

Psst…this one if you’ve been moved to ACG!