Reading and Writing Files with Python

45 minutes
  • 4 Learning Objectives

About this Hands-on Lab

Files are used for many things in programming, including storing and reading data as well as writing to the screen. In this hands-on lab, we’ll add a way to read and store information about custom classes, using a file as a flat database for our employee information. To feel comfortable completing this lab, you’ll want to know how to read and write to files (watch the “Interacting with Files” video from the [Certified Associate in Python Programming Certification](https://linuxacademy.com/cp/modules/view/id/470) course), use class methods (watch the “Custom Constructors, Class Methods, and Decorators” video from the [Certified Associate in Python Programming Certification](https://linuxacademy.com/cp/modules/view/id/470) course), and create and use class instances (watch the “Creating and Using Python Classes” 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:

Add `identifier` Attribute to `Employee` Instances and `__init__` Method

Before we start reading our Employee data from a file, we’re going to add an additional field to help us identify our employees so we can update them later. We’re going to call this field identifier, and we need to add it to the __init__ method as the final parameter with a default value of None. Here’s what our method will look like in our employee.py:

~/employee.py

class Employee:
    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

Now we can differentiate instances when reading and writing from a file.

Add `Employee.get_all` Class Method to Return a List of `Employee` Objects

The first class method we’re going to write will read in all of the employees from a file, with each line being a single employee. Here’s a list of all the things we need to do:

  1. Determine the file to read from. If no filename is given, we’ll use a default file name of employee_file.txt.
  2. Open the file, read each line, split the values on a , character, and add an additional value to the list that is the line number of the employees’ data.
  3. Create a new Employee instance using the data. The data will be stored in the same order as the parameters so we can unpack the data from the file as positional arguments using the * operator.
  4. Return the list of Employee objects.

Here’s what this will look like:

~/employee.py

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

        with open(file_name, "r") as f:
            lines = [
                line.strip("n").split(",") + [index + 1]
                for index, line in enumerate(f.readlines())
            ]

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

        return results

    # remainder of class was unchanged and omitted

Because each line is going to have a n character at the end, we’re going to remove that before we split the line into its values. Additionally, when we think of line numbers, we start counting at 1 instead of 0, so we’re going to make the identifier begin at 1.

Add `Employee.get_at_line` Class Method to Return a Single `Employee`

The get_at_line class method won’t be much different than get_all, except we want to return a single value. We’re going to need to take the line_number provided as an argument and subtract 1 from it so it can be used as an index of the list of lines. Here’s one way we could implement this function:

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

        with open(file_name, "r") as f:
            lines = [
                line.strip("n").split(",") + [index + 1]
                for index, line in enumerate(f.readlines())
            ]

        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

        with open(file_name, 'r') as f:
            line = f.readlines()[line_number - 1]
            attrs = line.strip("n").split(',') + [line_number]
            return cls(*attrs)

    # remainder of class was unchanged and omitted
Add `save` Instance Method to `Employee` Class to Write New Instances to the File

The last method we’re going to add is an instance method that will allow us to have an instance update or add its own in the "database" file. This method will need to do a few things. To make it more manageable to see what is going on, we’re going to place the logic that builds the line we’ll insert into the file into a separate "private" method (starting with a single underscore). Here’s what we need this method to do:

  1. Determine the file to save to, defaulting to the default_db_file value if no file_name is passed in.
  2. Open the database file in r+ mode so we don’t delete its contents if there are some.
  3. If the instance has an identifier, replace that line by getting all lines in a list and then replacing the appropriate index.
  4. If the instance does not have an identifier, add the line for this employee to the end of the list.
  5. seek back to the beginning of the file and use writelines to put all the lines back in the file.

Our method to create the line we write into the file will be called _database_line. Here are both of the methods:

~/employee.py

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

        with open(file_name, "r") as f:
            lines = [
                line.strip("n").split(",") + [index + 1]
                for index, line in enumerate(f.readlines())
            ]

        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

        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)

    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

        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)

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

We can test our implementation by running test_employee.py. If the implementation is correct, we won’t see any errors — but if things aren’t working correctly, we will see error messages that can hopefully help us.

python3.7 test_employee.py

Additional Resources

We currently have an Employee class we use to represent workers at our organization in our own code. As it stands right now, we can create instances of our class, but there's no way for us to persist this information to access after our current program has been shut down. We're going to utilize both class methods and instance methods to give our class the ability to read and write data from a file. As we're making changes to our Employee class, we can run the test_employee.py script to see if we've completely implemented everything we need. When we've implemented all of the necessary changes to the class, this script will run without any output or errors.

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!
Thanks for reaching out!

You’ll hear from us shortly. In the meantime, why not check out what our customers have to say about ACG?