Object-oriented student library This library management program can do following functions:


Add a book to the library  
Display all available books  
Lend books to students  
Return books to the library  
Display students who have yet to return a book


Question: How do you refactor this code so that it follows OOP, reads better, is manageable and is pythonic? How can I write name functions and classes better? How do you know which data structure you need to use so as to manage data effectively?

Library class that contains a list of books

class Library:
    def __init__(self):
        self._books = []

    def add_book(self, new_book):
        self._books.append(new_book)

    def display_books(self):
        if self._books:  # if list of books is not empty
            print("The books we have made available in our library are:\n")
            for book in self._books:
                print(book)
        else:
            print("Sorry, we have no books available in the library at the moment")

    def lend_book(self, requested_book):
        if requested_book in self._books:
            print("You have now borrowed \"%s\" " % requested_book)
            self._books.remove(requested_book)

        else:
            print("Sorry, \"%s\" is not there in our library at the moment" % requested_book)


StudentDatabase class (container for all students)

class StudentDatabase:
    def __init__(self):
        self._books = {}

    def get_student(self, name):
        if name not in self._books:
            return "Not Found"

        return self._books[name]

    def borrow_book(self, name, book):
        if name not in self._books:
            self._books[name] = list(book)
        else:
            self._books[name].append(book)

    def return_book(self, name, book):
        if book not in self._books[name]:
            print("You don't seem to have borrowed \"%s\"" % book)
        else:
            self._books[name].remove(book)

    def display_students_with_books(self):
        for name, books in self._books.items():
            if books:
                print("%s: %s" % (name, books))


Follow-up : Object-oriented student library
In addition to the below, I recommend reading Maarten Fabré's answer.



Edge case

Consider the following scenario:

book_tracking = StudentDatabase()
book_tracking.return_book("Student A", "Moby Dick")
# KeyError: 'Student A'


However, we don't need to do a try-except block to catch the student who never handled books before trying to return a book; Python provides defaultdict in the collections module to help us with this very problem:

from collections import defaultdict

class StudentDatabase:
    def __init__(self):
        self._books = defaultdict(list)

    # ...


Now, if anything tries to access an unassigned key of _books, it will simply return an empty list. This can subsequently simplify borrow_book():

    def borrow_book(self, name, book):
        self._books[name].append(book)


Interclass operability

Library and StudentDatabase don't seem to interact at all. If they're related, the student database listing book borrows should inform the library when a book is borrowed or returned. The most versatile way to do this at this stage is to add a library parameter to relevant methods.

class StudentDatabase:
    # ...

    def borrow_book(self, name, book, library):
        if library.lend_book(book):
            self._books[name].append(book)

    def return_book(self, name, book, library):
        if book not in self._books[name]:
            print("You don't seem to have borrowed \"%s\"" % book)
        else:
            library.add_book(book)
            self._books[name].remove(book)


This will require returning False when the book is not found:

class Library:

    # ...

    def lend_book(self, requested_book):
        if requested_book in self._books:
            print("You have now borrowed \"%s\" " % requested_book)
            self._books.remove(requested_book)
            return True

        else:
            print("Sorry, \"%s\" is not there in our library at the moment" % requested_book)
            return False


The implementation you currently have is very simple, so there's not much else to discuss in terms of design: it becomes more relevant when you have more moving parts. I do find it somewhat odd that it's only tracking by student, and not by book (i.e. Library could know how many copies of each book it had (int), to whom it's checked (a list perhaps with a custom CheckedOut class to store the person and the due date), and any other information pertaining to that book. A BookStatus class could store everything related to each book; the library would then contain a list of BookStatus objects.

Checking if a sequence is empty.

(Note I previously advocated using len(self._books) &gt; 0 until Maarten Fabré (along with three people upvoting his comment) pointed out this suggestion contradicted PEP-8) The relevant passage is excerpted below.


  For sequences, (strings, lists, tuples), use the fact that empty sequences are false.


Format string literals

You seem to be using printf-style string formatting. In Python 3, format strings are more flexible and would probably be considered more Pythonic. Python 3.6 adds format string literals, which are very compact. Here are some example uses:


Original: "You have now borrowed \"%s\" " % requested_book


With f-string: f"You have now borrowed \"{requested_book}\"."

Original: "Sorry, \"%s\" is not there in our library at the moment" % requested_book


With f-string: f"Sorry, \"{requested_book}\" is not there in our library at the moment"

Original: "You don't seem to have borrowed \"%s\"" % book


With f-string: f"You don't seem to have borrowed \"{book}\""

Original: "%s: %s" % (name, books)


With f-string: f"{name}: {books}"



Result

from collections import defaultdict

class Library:
    def __init__(self):
        self._books = []

    def add_book(self, new_book):
        self._books.append(new_book)

    def display_books(self):
        if len(self._books) &gt; 0:
            print("The books we have made available in our library are:\n")
            for book in self._books:
                print(book)
        else:
            print("Sorry, we have no books available in the library at the moment")

    def lend_book(self, requested_book):
        if requested_book in self._books:
            print(f"You have now borrowed \"{requested_book}\".")
            self._books.remove(requested_book)
            return True

        else:
            print(f"Sorry, \"{requested_book}\" is not there in our library at the moment")
            return False

class StudentDatabase:
    def __init__(self):
        self._books = defaultdict(list)

    def get_student(self, name):
        if name not in self._books:
            return "Not Found"

        return self._books[name]

    def borrow_book(self, name, book, library):
        if library.lend_book(book):
            self._books[name].append(book)

    def return_book(self, name, book, library):
        if book not in self._books[name]:
            print(f"You don't seem to have borrowed \"{book}\"")
        else:
            library.add_book(book)
            self._books[name].remove(book)

    def display_students_with_books(self):
        for name, books in self._books.items():
            if books:
                print(f"{name}: {books}")