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:
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
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
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
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}"
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
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
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}"
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}"
And:
@dataclass
class Newspaper(LibraryItem):
title: str
date: date
def description(self) -> str:
return f"{self.title} newspaper issued on {self.date}"
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
And:
@dataclass
class Member:
member_id: int
name: str
borrowed_items: list[Borrowable] = field(default_factory=list)
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()
print(book)
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
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)
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)
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
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)
And:
def borrow(
self,
item: Borrowable
) -> None:
...
And:
returned_date: date | None = None
Running:
mypy .
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)