Embercode
✕
Object-Oriented ProgrammingLesson 6 of 17

55 min read

Module assignment: Library desk in Python

Why this assignment

The five lessons in this module each covered one idea: classes and __init__, encapsulation, inheritance, polymorphism and abstract base classes, and protocols with class attributes and constants. In a real job these ideas arrive together. A single feature, such as a library's checkout desk, needs a class for each kind of thing, internal attributes to keep the rules safe, a base class for the shared parts, a protocol or base for a shared ability, and a sort for the report at the end.

This assignment asks you to build exactly that. The program is a lending desk for a small library. It has books and DVDs, members who borrow them, and a list report. You will write the program in steps, and each step has a small check you can run before you go on. The final part of the assignment is a full reference solution, so you can compare your work with one correct answer after you have tried it yourself.

You do not need new Python knowledge for this. You need to use what the module taught, and to read error messages carefully.

The brief

A library has a set of items. Each item has a number (its id), a title, and a flag that says whether it is on loan. There are two kinds of item, a book and a DVD. They share everything above, but they differ in two ways. Each kind has a name (book or dvd), and each kind charges a different fine for each day an item comes back late. A book charges 10 per day late. A DVD charges 25 per day late.

A member is a person who borrows items. Use the member's name as the identity. A member may hold at most two items at a time. A member is created the first time their name appears in a checkout command.

The library processes a list of commands. There are three kinds:

  • out NAME ID means member NAME borrows the item with that id.
  • in ID DAYS means the item with that id comes back, DAYS days late. DAYS is 0 or more.
  • list prints every item, sorted by title, with its kind and status.

The exact messages

Your output must match these messages exactly, including capital letters and spaces. A checker compares text, so a missing space is a wrong answer.

SituationMessage
Checkout succeedsNAME borrowed TITLE
The id does not exist (checkout or check-in)no item ID
Checkout of an item that is already on loanitem ID is on loan
The member already holds two itemsNAME has too many items
Check-in succeeds with no fine (0 days late)returned TITLE no fine
Check-in succeeds with a finereturned TITLE fine AMOUNT
Check-in of an item that is not on loanitem ID is not on loan
One line per item for listTITLE KIND STATUS, where STATUS is on loan or available

The fine amount is the number of days late times the fine per day for the item's kind. An item that is returned early or on time gives no fine. A late item with a fine of zero is not possible, because the fine per day is always above zero.

The order of checks

When an item is checked out, check things in this order: first whether the id exists, then whether the item is already on loan, then whether the member has room. The order changes the message. An item that does not exist gives no item ID, even if the member is also at the limit.

For check-in, check first whether the id exists, then whether the item is on loan.

The input format

The program reads from standard input, which is the input box when you run it in the editor. The input has three parts:

  1. A number N, then N item lines. Each item line is a kind (book or dvd), an id, and a title. The title is one word with no spaces.
  2. A number M, then M command lines. Each command is one of the three forms above.

Each line ends with a newline, and the values on a line are separated by spaces. The function input() reads one line as text. Split the line with split() to get the values, and use int() for numbers.

Sample inputs and outputs

Your program must produce these outputs exactly. Try both samples. The first one exercises the member limit and a fine. The second one exercises a check-in with no fine, a check-in of an item that is not on loan, and a list with an item still on loan.

Sample 1 input:

4
book 1 Dune
dvd 2 Alien
book 3 Emma
book 4 Atlas
6
out Ana 1
out Ben 1
out Ana 2
out Ana 3
in 1 3
list

Sample 1 output:

Ana borrowed Dune
item 1 is on loan
Ana borrowed Alien
Ana has too many items
returned Dune fine 30
Alien dvd on loan
Atlas book available
Dune book available
Emma book available

Trace the first few lines so you know what to expect. Ana borrows Dune, which succeeds. Ben tries to borrow the same item, which is still on loan, so he gets the on-loan message. Ana then borrows Alien, which makes two items. Her third attempt, for Emma, hits the limit, and the message says so. The item id 3 is not checked out, because the member check failed. Check-in of Dune three days late costs 3 times 10, which is 30. The list then shows Alien still on loan, because Ana has not returned it.

Sample 2 input:

3
dvd 7 Heat
book 8 Ants
book 9 Zed
6
out Cy 7
out Cy 8
out Cy 9
in 7 0
in 7 2
list

Sample 2 output:

Cy borrowed Heat
Cy borrowed Ants
Cy has too many items
returned Heat no fine
item 7 is not on loan
Ants book on loan
Heat dvd available
Zed book available

Notice the line item 7 is not on loan. Heat was returned on the previous command, so the second check-in of id 7 finds an item that is no longer out. Notice also that Ants is listed as on loan, because Cy never returned it. The list is sorted by title, so Ants comes first, even though its id is 8.

Starting point

Begin with a starter file that runs. It has the class names and the TODO comments that show where your code goes. Type nothing into the input yet. It prints one line so you know it runs.

Python · editable

The output is:

starter ready

The starter has a few things you should notice. The Item class is an abstract base class, because an item must be a book or a DVD, never just an item. The Book and Dvd classes are the concrete children. The Library class is where the rules live, and main only reads input and calls the library. Keep that split in your own code. main should be short.

The if __name__ == "__main__": line runs main only when the file is run directly. It is the Python form of Java's main entry point.

Step 1: items and sorting

Build the item classes first, and check that they sort the way the brief says. The check below creates three items, sorts them, and prints them. Your real Item class will have more attributes, but the sorting rule is the same: title first, then id as the tie-breaker.

Python · editable

The output is:

Alien id 2
Dune id 1
Dune id 5
fine for 2 days late on a dvd: 50

The two Dunes have the same title, so the id decides their order. Dune id 1 comes before Dune id 5. The tuple (self._title, self._id) does the work. Python compares the first items of two tuples, and only when they are equal does it compare the second items. Python integers never overflow, so there is no subtraction risk to watch for.

Step 2: the member limit

The next check tests the rule for members. A member can hold at most two items, and the third checkout must fail. The example uses only the titles, so you can focus on the rule.

Python · editable

The output is:

Ana borrowed Dune
Ana borrowed Alien
Ana has too many items

The LIMIT value is a class attribute, because it is the same rule for every member. The _borrowed list is internal, so no code outside Member should add a title and break the limit. can_borrow is the way to ask whether there is room, and add is the way to take an item. Your real Member class will keep the item objects instead of titles, and the logic is the same.

Step 3: reading the items from input

The last check reads the item part of the input, which is the first part of the input format. It counts the books and the DVDs. Type the sample item lines into the input, one line at a time:

4
book 1 Dune
dvd 2 Alien
book 3 Emma
book 4 Atlas
n = int(input())
books = 0
dvds = 0
for _ in range(n):
    kind, item_id, title = input().split()
    print(f"read {kind} {item_id} {title}")
    if kind == "book":
        books += 1
    else:
        dvds += 1
print(f"books {books}")
print(f"dvds {dvds}")

The output is:

read book 1 Dune
read dvd 2 Alien
read book 3 Emma
read book 4 Atlas
books 3
dvds 1

Each item line has three values, and the loop reads them in the same order they appear. The kind tells you which class to build. Your real code will create a Book or a Dvd object in that branch and add it to the library, instead of counting.

Requirements checklist

When you think the program is finished, go through this list. Each line must be true. A missing item costs you points in the rubric below.

  • Item is an abstract base class, and Book and Dvd inherit from it.
  • Item defines __lt__, and sorts by title, then by id.
  • All attributes of Item, Member and Library are internal, with a leading underscore, and any value other code needs is read through a property or a method.
  • Each kind of item gives its own name and fine per day through an abstract method. The shared code is not copied into Book and Dvd.
  • Member.LIMIT is a class attribute with the value 2.
  • The library keeps an item dictionary by id, a member dictionary by name, and a loan dictionary from item id to the member who holds it.
  • The checkout checks run in the order the brief gives: id, then on loan, then member limit.
  • The check-in returns a fine of days times the fine per day, or no fine when the days are 0.
  • The list command sorts by title and prints kind and status.
  • The program reads all input with input() and prints nothing else, so each line of output matches the table.
  • Both samples produce exactly the output shown in this lesson.
  • The program has one main function, called from if __name__ == "__main__":.

Grading rubric

Your grade depends on the outputs and on the design. The rubric describes what a 60, 80 and 100 percent solution looks like.

60 percent. The program runs. It handles checkout of an item that exists and is free, and check-in of an item that was on loan. It prints the right text for those two cases. It may get the member limit wrong, may not sort the list, or may compute the fine with a fixed number instead of the item's rule. Classes may be missing, or may put all the logic in main. Some attributes may be public.

80 percent. All commands work for ordinary input. The member limit is correct, and so are the messages for unknown ids and items already on loan. The fine is computed from each item's own kind. The list prints each item with its status. The sort may be by title only, so two items with the same title come out in an unpredictable order. The design uses inheritance for Book and Dvd, but the abstract method for the fine may be missing, so the fine code is written in Library with an if on the kind. Both samples pass.

100 percent. Both samples pass, and the program also works for the edge cases the brief describes. The sort uses the title and then the id. The attributes are internal. The shared behaviour is in Item, and the per-kind rules are abstract methods that Book and Dvd implement. The Member class owns its limit, and the Library does not check the count directly. The code is short, and each class has one clear job. The program reads input with input() and prints the exact text. There are no unused attributes, no duplicated code, and no leftover print lines for debugging.

Two things cost marks at every level. A program that fails to run scores almost nothing, so run it after every change. A program that prints extra text, such as a prompt or a debug line, fails the output check even when the logic is right.

Hints

Try to finish each step before you open a hint. Each hint is hidden until you open it.

Show the hint 1: start with the item attributes and __init__

Write the Item __init__ first, with self._id and self._title. Add read-only properties for both. Then add self._on_loan = False with a property and a setter. The checkout and check-in code sets the setter. Do not make _on_loan a public name.

Show the hint 2: put the fine rule in the subclass

Declare fine_per_day as an abstract method in Item. Then Book returns 10 and Dvd returns 25. The check-in code calls item.fine_per_day() and does not ask what kind the item is. If your check-in code has an if on the kind, move that rule into the subclass.

Show the hint 3: the library needs three dictionaries

Use three dictionaries: one for the items by id, one for the members by name, and one for the loans, which says who holds each item. When a name is seen for the first time, create the member and store it in the members dictionary. When an item is checked out, store the member in the loans dictionary. When it is checked in, remove it from the loans dictionary and take the item off the member's list.

Show the hint 4: the order of the checks

Use early returns. Each check that fails returns its message at once, so the next check does not run. For checkout, the order is: the item exists, then the item is not on loan, then the member can borrow. The success path comes last. This order is why an unknown id always wins over the member limit.

Show the hint 5: the fine and the sort

For the fine, write days_late * item.fine_per_day() if days_late > 0 else 0, and check whether the fine is 0 before building the message. For the list, sorted(items.values()) returns a new list, sorted by your __lt__. The sort itself needs no extra code.

Show the hint 6: testing before you submit

Run sample 1 with its input typed into the input box, and compare each line with the expected output. If one line is wrong, find the first wrong line and trace the command that produced it. Most errors come from the order of the checks or from the fine calculation. Then run sample 2, which tests the check-in cases.

Reference solution

Try the assignment before you open this. The solution is complete and it passes both samples. Read it after you have a version of your own, and compare the design. Your code may differ in names and in how you split the classes, and it is still correct if it produces the same output.

Show the answer

from abc import ABC, abstractmethod
 
 
class Item(ABC):
    def __init__(self, item_id, title):
        self._id = item_id
        self._title = title
        self._on_loan = False
 
    @property
    def id(self):
        return self._id
 
    @property
    def title(self):
        return self._title
 
    @property
    def on_loan(self):
        return self._on_loan
 
    @on_loan.setter
    def on_loan(self, value):
        self._on_loan = value
 
    @abstractmethod
    def kind(self):
        ...
 
    @abstractmethod
    def fine_per_day(self):
        ...
 
    def __lt__(self, other):
        return (self._title, self._id) < (other._title, other._id)
 
 
class Book(Item):
    def kind(self):
        return "book"
 
    def fine_per_day(self):
        return 10
 
 
class Dvd(Item):
    def kind(self):
        return "dvd"
 
    def fine_per_day(self):
        return 25
 
 
class Member:
    LIMIT = 2
 
    def __init__(self, name):
        self._name = name
        self._borrowed = []
 
    @property
    def name(self):
        return self._name
 
    def can_borrow(self):
        return len(self._borrowed) < Member.LIMIT
 
    def add(self, item):
        self._borrowed.append(item)
 
    def remove(self, item):
        self._borrowed.remove(item)
 
 
class Library:
    def __init__(self):
        self._items = {}
        self._members = {}
        self._loans = {}
 
    def add_item(self, item):
        self._items[item.id] = item
 
    def checkout(self, member_name, item_id):
        item = self._items.get(item_id)
        if item is None:
            return f"no item {item_id}"
        if item.on_loan:
            return f"item {item_id} is on loan"
        if member_name not in self._members:
            self._members[member_name] = Member(member_name)
        member = self._members[member_name]
        if not member.can_borrow():
            return f"{member_name} has too many items"
        item.on_loan = True
        member.add(item)
        self._loans[item_id] = member
        return f"{member_name} borrowed {item.title}"
 
    def checkin(self, item_id, days_late):
        item = self._items.get(item_id)
        if item is None:
            return f"no item {item_id}"
        if not item.on_loan:
            return f"item {item_id} is not on loan"
        member = self._loans.pop(item_id)
        member.remove(item)
        item.on_loan = False
        fine = days_late * item.fine_per_day() if days_late > 0 else 0
        if fine == 0:
            return f"returned {item.title} no fine"
        return f"returned {item.title} fine {fine}"
 
    def sorted_items(self):
        return sorted(self._items.values())
 
 
def main():
    lib = Library()
    n = int(input())
    for _ in range(n):
        kind, item_id, title = input().split()
        if kind == "book":
            lib.add_item(Book(int(item_id), title))
        else:
            lib.add_item(Dvd(int(item_id), title))
    m = int(input())
    for _ in range(m):
        parts = input().split()
        cmd = parts[0]
        if cmd == "out":
            print(lib.checkout(parts[1], int(parts[2])))
        elif cmd == "in":
            print(lib.checkin(int(parts[1]), int(parts[2])))
        elif cmd == "list":
            for item in lib.sorted_items():
                status = "on loan" if item.on_loan else "available"
                print(f"{item.title} {item.kind()} {status}")
 
 
if __name__ == "__main__":
    main()

Read the solution for these design choices.

Item holds everything the two kinds share: the id, the title, the loan flag and the sort order. Book and Dvd only say what makes them different, which is the name and the fine. If the library later gets a third kind, such as a magazine, you add one class with two small methods, and the checkout and check-in code does not change. The input reader in main needs one more branch.

Member owns its rule. The LIMIT value and the can_borrow method live together, so the rule is in one place. The Library asks the member, and it never counts the member's items itself.

Library holds three dictionaries. The items dictionary answers "which item has this id". The members dictionary answers "who is this name". The loans dictionary answers "who is holding this item", which is what the check-in needs, because the check-in command gives only the item id.

The checkout method returns a message in every case. That makes the output of main simple: it prints whatever the library returns. The checks run in the order the brief gives, so an unknown id is reported before the on-loan check, and both come before the member limit.

The sorted_items method returns a new list. The sort changes only that list, and the dictionary keeps its own order.

Look at the way the fine is computed. The library never asks for the kind of item. It asks the item for fine_per_day(), and the subclass answers. A new kind needs no change here.

What to submit

Put your final program in the editor, with all of its classes in one file. Run it with sample 1 and sample 2 typed into the input, and compare the output line by line. If both match, the program is ready. If one line is off, go back to the checklist and check the item it describes.

When you finish, write one paragraph for yourself that answers three questions. Which class holds the fine rule, and why there? Which attribute would you make public if you were in a hurry, and what would go wrong? Which check runs first in checkout, and what would change if you moved it to the end? These are the questions an interviewer is likely to ask about this design.

What this assignment tests

The assignment touches the five lessons before it. The Item and Member initializers and the self references come from the first lesson. The internal attributes, properties and limit check come from encapsulation. The Book and Dvd classes use inheritance and an abstract method. The __lt__ sort and the class attribute LIMIT come from the fifth lesson.

If you can build this program from the brief without the solution, you have the core skills of this module. If you needed the hints, go back to the lesson that matches the step you got stuck on, and read its worked examples again.

Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.