DEV Community

Nattar Kani Murugan
Nattar Kani Murugan

Posted on

Day 4: Building a Library Management System with Python OOP | 10 Weeks Challenge | To AI Engineer

Okay, Nattar.

The challenge today was:

Model a library-management system with 6+ classes, ABCs and dataclasses. It must pass mypy clean.

This one felt different from the previous three days.

Day 1: Text Analyzer.

Day 2: Comprehensions + generators.

Day 3: Decorators + my own Python package.

Today, I had to stop thinking only about individual functions and start thinking about:

Objects.

Relationships.

Responsibilities.

And honestly...

my brain had a few moments of:

GIF

First, I needed a map

Before creating classes, I wanted to understand what actually exists inside a library management system.

So I created a mind map of the overall architecture and features.

Library Management System

├── Library
│ ├── Items
│ ├── Members
│ └── Loans

├── Library Items
│ ├── Book
│ ├── Magazine
│ ├── Newspaper
│ └── DVD

├── People
│ ├── Member
│ └── Author

├── Borrowing
│ ├── Borrow
│ └── Return

└── Abstractions
├── LibraryItem
└── Borrowable

Step 1: Start with abstraction

I created an abstract base class for library items.

from abc import ABC, abstractmethod

class LibraryItem(ABC):

    @abstractmethod
    def description(self) -> str:
        pass
Enter fullscreen mode Exit fullscreen mode

The idea is simple.

Every library item should be able to describe itself.

A book can describe itself. A magazine can describe itself. A newspaper can describe itself. A DVD can describe itself.

But LibraryItem itself doesn't need to know exactly how. That's where abstraction comes in.

Step 2: What can actually be borrowed?

Then I created another abstraction:

class Borrowable(ABC):

    @abstractmethod
    def borrow(self) -> None:
        pass

    @abstractmethod
    def return_item(self) -> None:
        pass
Enter fullscreen mode Exit fullscreen mode

This was an interesting distinction. Not every library item necessarily needs to be borrowable.

For example, I could have:

Book       → Borrowable
DVD        → Borrowable
Magazine   → Maybe not
Newspaper  → Maybe not
Enter fullscreen mode Exit fullscreen mode

So instead of putting borrow() directly into every library item, I created a separate abstraction for borrowable behaviour.

Step 3: Creating the actual items

@dataclass
class Book(LibraryItem, BorrowableItem):
    title: str
    isbn: str
    author: Author
    _available: bool = True

    def description(self) -> str:
        return f"{self.title} by {self.author}"
Enter fullscreen mode Exit fullscreen mode

A Book is:

  • A LibraryItem
  • A BorrowableItem

This was my first hands-on experience with multiple inheritance in this project.

Wait... why multiple inheritance?

This was one of those moments where I had to stop and think. A book is a library item. But a book also has the behaviour of something that can be borrowed.

So I modelled that relationship as:

Book
├── LibraryItem
└── BorrowableItem
Enter fullscreen mode Exit fullscreen mode

The interesting part is that BorrowableItem itself implements the actual borrowing behaviour.

class BorrowableItem(Borrowable):

    _available: bool

    @property
    def available(self) -> bool:
        return self._available

    @available.setter
    def available(self, value: bool) -> None:
        if not isinstance(value, bool):
            raise TypeError(
                "You are not allowed to change this"
            )

        self._available = value

    def borrow(self) -> None:
        if not self.available:
            raise ValueError("Item not available")

        self.available = False

    def return_item(self) -> None:
        self.available = True
Enter fullscreen mode Exit fullscreen mode

Now the borrowing logic doesn't need to be repeated inside Book and DVD.

Step 4: DVD can use the same behaviour

A DVD can also be borrowed.

@dataclass
class DVD(LibraryItem, BorrowableItem):
    title: str
    duration: int
    _available: bool = True

    def description(self) -> str:
        return f"{self.title} runtime is {self.duration}"
Enter fullscreen mode Exit fullscreen mode

The DVD doesn't need to implement borrow() and return_item() again. It gets that behaviour from BorrowableItem. This was a good reminder that inheritance isn't only about sharing data.

It can also be about sharing behaviour.

Step 5: Not everything needs borrowing

I also created classes for things like magazines and newspapers.

For example:

@dataclass
class Magazine(LibraryItem):
    title: str
    edition: int

    def description(self) -> str:
        return f"Magazine {self.title} of edition {self.edition}"
Enter fullscreen mode Exit fullscreen mode

And:

@dataclass
class Newspaper(LibraryItem):
    title: str
    date: date

    def description(self) -> str:
        return f"{self.title} newspaper issued on {self.date}"
Enter fullscreen mode Exit fullscreen mode

Notice something?

They inherit from LibraryItem. But not BorrowableItem. That was intentional.

Step 6: Dataclasses saved me from boilerplate

I used @dataclass quite heavily.

For example:

@dataclass
class Author:
    name: str
    birth_year: int
Enter fullscreen mode Exit fullscreen mode

And:

@dataclass
class Member:
    member_id: int
    name: str
    borrowed_items: list[Borrowable] = field(default_factory=list)
Enter fullscreen mode Exit fullscreen mode

Instead of manually writing constructors for every class, dataclasses generate a lot of that boilerplate for me.

And field(default_factory=list) was particularly useful.

I don't want every Member to accidentally share the same list of borrowed items. So each member gets their own list.

Step 7: I finally used some dunder methods

I also wanted to experiment with dunder methods.

For Book, I added __str__():

def __str__(self) -> str:
    return self.description()
Enter fullscreen mode Exit fullscreen mode
print(book)
Enter fullscreen mode Exit fullscreen mode

can give me something meaningful instead of a default object representation.

I also experimented with __eq__().

For books, I decided that the ISBN should determine equality:

def __eq__(self, other) -> bool:
    if isinstance(other, Book):
        return self.isbn == other.isbn

    return NotImplemented
Enter fullscreen mode Exit fullscreen mode

So two book objects with the same ISBN can be considered equal. That made me realise something:

Dunder methods aren't just strange methods with double underscores.

They let my objects behave more naturally with Python's built-in operations.

Step 8: Who manages everything?

Now I needed something to actually manage the library.

Enter:

@dataclass
class Library:
    items: list[LibraryItem] = field(default_factory=list)
    members: list[Member] = field(default_factory=list)
    loans: list[Loan] = field(default_factory=list)
Enter fullscreen mode Exit fullscreen mode

The Library contains:

  • Library items
  • Members
  • Loans

This is where composition came into the picture.

A library isn't a member.

A library isn't a book.

A library has members, books and loans.

That distinction helped me understand the difference between composition and inheritance much better.

Step 9: Borrowing an item

The library can handle borrowing:

def borrow_item(
    self,
    member: Member,
    item: Borrowable
) -> None:

    if member not in self.members:
        raise ValueError("Member not registered")

    if item not in self.items:
        raise ValueError("Item is not in the library")

    member.borrow(item)

    loan = Loan(
        member=member,
        item=item,
        borrowed_date=date.today()
    )

    self.loans.append(loan)
Enter fullscreen mode Exit fullscreen mode

Now there are multiple objects involved.

The:

Library

knows about the:

Member

and the:

Item

and creates a:

Loan

This is where the architecture started feeling like an actual system rather than a collection of unrelated classes.

Step 10: A loan is its own object

Instead of storing borrowing information randomly, I created a Loan class:

@dataclass
class Loan:
    member: Member
    item: Borrowable
    borrowed_date: date
    returned_date: date | None = None

    @property
    def is_active(self) -> bool:
        return self.returned_date is None
Enter fullscreen mode Exit fullscreen mode

This was another useful design decision.

A loan has its own information:

  • Who borrowed it?
  • What did they borrow?
  • When did they borrow it?
  • Has it been returned?

Instead of putting all of that information into Member or Library, I gave it its own object.

And then... mypy

The challenge had one more requirement:

It must pass mypy clean.

So I had to pay attention to type hints throughout the code.

For example:

def add_item(self, item: LibraryItem) -> None:
    self.items.append(item)
Enter fullscreen mode Exit fullscreen mode

And:

def borrow(
    self,
    item: Borrowable
) -> None:
    ...
Enter fullscreen mode Exit fullscreen mode

And:

returned_date: date | None = None
Enter fullscreen mode Exit fullscreen mode

Running:

mypy .
Enter fullscreen mode Exit fullscreen mode

became part of my workflow.

And honestly, I liked this.

What did I actually learn?

Today's challenge gave me hands-on practice with:

  • Classes
  • Objects
  • Inheritance
  • Multiple inheritance
  • Abstraction
  • Abstract Base Classes
  • Composition
  • Dataclasses
  • field(default_factory=...)
  • Properties
  • Getters and setters
  • Dunder methods
  • __str__
  • __eq__
  • Type hints
  • mypy
  • Domain modelling

But the biggest lesson wasn't any individual Python feature.

It was this:

Before writing classes, understand the objects and relationships in the problem.

The mind map helped more than I expected

This is probably my favourite part of today's challenge.

I created the architecture mind map before or alongside the implementation.

Day 4: Done. ✅

Four days into the challenge.

And the projects are slowly getting bigger.

I'm starting to realise that learning Python isn't just about learning more syntax. It's also about learning how to model problems.

And today, I finally spent more time thinking about the design before thinking about the code. That's progress.

So...

One more day done.

Keep going, Nattar. 🚀

🔗 The project

I've added today's Library Management System to GitHub as part of my 10 Week AI Challenge.

👉 View the Library Management System on GitHub

See you on Day 5.

Top comments (0)