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 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

Using the Terminal To Complete the Lab

There are a couple of ways to get in and work with the code. One is to use the credentials provided in the lab, log in with SSH, and use a text editor in the terminal, such as Vim.

Note: When copying and pasting code into Vim from the lab guide, first enter :set paste (and then i to enter insert mode) to avoid adding unnecessary spaces and hashes. To save and quit the file, press Escape followed by :wq. To exit the file without saving, press Escape followed by :q!.

Using VS Code To Complete the Lab

You can also access the lab using VS Code in the browser. If you'd like to go this route, then follow the steps below:

  1. Navigate to the public IP address of the workstation server (provided in your lab credentials) on port 8080, using http (e.g., http://PUBLIC_IP:8080).
  2. If you receive a notification indicating the connection is not secure, click Advanced. Then, proceed to the server.
  3. Use the password provided in your lab credentials.

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.

Sign In
Welcome Back!

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

Get Started
Who’s going to be learning?