Working with multiple files

Day 21: Exercise Solutions

Python Guru with a screen instead of a face, typing on a computer keyboard with a blue background to match the day 21 image.

Here's our solution for the Day 21 Exercise in 30 Days Of Python! Remember to tackle the exercise yourself before reading through our solution.

1) Splitting Code Into Files

This exercise was as much an exercise in reading and understanding code, as much as splitting code.

But really, that's the whole point! When you read and understand code, you can start thinking of code as being composed of different sections that do different things.

For us, the different sections were relatively clear: we have a user-facing set of functions and a data-facing set of functions.

The functions that interact with the user were:

  • menu()
  • prompt_add_book()
  • list_books()
  • prompt_read_book()
  • prompt_delete_book()

And the functions that interact with the data storage (a JSON file in this case):

  • create_book_table()
  • get_all_books()
  • insert_book()
  • save_all_books()
  • mark_book_as_read()
  • delete_book()

Will this in mind, we could split the file into two files: one for each section of the code.

We would end up with something like main.py containing user-facing code:

main.py
import database

USER_CHOICE = """
Enter:
- 'a' to add a new book
- 'l' to list all books
- 'r' to mark a book as read
- 'd' to delete a book
- 'q' to quit

Your choice: """

def menu():
    database.create_book_table()
    user_input = input(USER_CHOICE)
    while user_input != 'q':
        if user_input == 'a':
            prompt_add_book()
        elif user_input == 'l':
            list_books()
        elif user_input == 'r':
            prompt_read_book()
        elif user_input == 'd':
            prompt_delete_book()

        user_input = input(USER_CHOICE)

def prompt_add_book():
    name = input('Enter the new book name: ')
    author = input('Enter the new book author: ')

    database.insert_book(name, author)

def list_books():
    for book in database.get_all_books():
        # book[3] will be a falsy value (0) if not read
        read = 'YES' if book['read'] == '1' else 'NO'
        print(f"{book['name']} by {book['author']} — Read: {read}")

def prompt_read_book():
    name = input('Enter the name of the book you just finished reading: ')

    database.mark_book_as_read(name)

def prompt_delete_book():
    name = input('Enter the name of the book you wish to delete: ')

    database.delete_book(name)

menu()

And a database.py file that contains the data-storage code:

database.py
import json

BOOKS_FILE = 'books.json'

def create_book_table():
    try:
        with open(BOOKS_FILE, 'x') as file:
            json.dump([], file)  # initialize file as empty list
    except FileExistsError:
        pass

def get_all_books():
    with open(books_file, 'r') as file:
        return json.load(file)

def insert_book(name, author):
    books = get_all_books()
    books.append({'name': name, 'author': author, 'read': False})
    save_all_books(books)

def save_all_books(books):
    with open(books_file, 'w') as file:
        json.dump(books, file)

def mark_book_as_read(name):
    books = get_all_books()
    for book in books:
        if book['name'] == name:
            book['read'] = '1'
    save_all_books(books)

def delete_book(name):
    books = get_all_books()
    books = [book for book in books if book['name'] != name]
    save_all_books(books)

If you ended up with a different way of splitting code, don't worry! There isn't really a right and wrong way. However, we'd love to chat with you about it over in our Discord server. We both might learn something new!

Thank you for joining us for this exercise, and remember to also tackle today's project. It's quite a big one today!

Good luck, and we'll see you there!

Additional Resources

If you're interested in learning more about this piece of syntax in the list_books function:

read = 'YES' if book['read'] == '1' else 'NO'

We have a post you can look at on our blog.