Unit 4 Project: Build a Small Object Model
Put classes, methods, and state together in one mini-system
In this lesson
In this project you’ll build a small object model in Python — a set of cooperating objects — applying everything from Unit 4. You’ll design it, then build it up one class and one method at a time. The goal isn’t a huge program; it’s seeing classes, methods, and state work together in something that actually runs.
Explain it like I’m 5
A small object model is a tiny pretend world where each thing knows its own data and its own actions — and they work together to get something done.
Design before you code
Start by naming the things in your domain and what each one knows and does. For a library: a Book knows its title and whether it’s borrowed; a Library holds many books and can add, borrow, and list them. Sketching this first keeps each class small and focused — a classic OOP habit.
Build it one method at a time
Add classes and methods incrementally, checking each as you go rather than writing everything and hoping. Notice how the Library composes Book objects — it holds them in a list rather than inheriting from them, because a library has books, it isn’t a kind of book.
class Book:
def __init__(self, title):
self.title = title
self.borrowed = False
class Library:
def __init__(self):
self.books = []
def add(self, title):
self.books.append(Book(title))
def borrow(self, title):
for book in self.books:
if book.title == title and not book.borrowed:
book.borrowed = True
return True
return False
def available(self):
return [book.title for book in self.books if not book.borrowed]
lib = Library()
lib.add("Python 101")
lib.add("Clean Code")
lib.borrow("Python 101")
print(lib.available())
['Clean Code']
Test by hand, then refactor
Drive your objects through a few realistic steps and check the results — borrow a book twice and confirm the second attempt fails, list what’s left, and so on. This is exactly the manual testing habit from debugging basics. If you find yourself copying the same dictionary-shuffling code in several places, that repetition is the signal to fold it into a method on a class.
Common mistake: Building many classes before the first one works
It’s tempting to design the whole system up front and write every class at once.
Get one class running end to end, then add the next. Each working piece tells you whether the design holds up.
Common mistake: Mixing input() prompts into every method
Asking the user right where you need the value feels direct.
Pass values in as arguments and return results; do the input() and print() in one outer layer. Your objects stay testable and reusable.
Common mistake: Using inheritance when two objects share only a tiny detail
Any shared field can make inheritance look appropriate.
Prefer composition — one object holding another — unless the child genuinely is a kind of the parent. A Library has Books; it isn’t a kind of book.
In a library system, which parts make good classes?
The ‘things’ in the domain — Book and Library — are the natural classes; each owns its own data and actions.
Where should user input live in an object-oriented program?
Keeping input() out of your objects keeps them reusable and easy to test.
Why does Library hold Book objects rather than inherit from Book?
Composition models a ‘has-a’ relationship; inheritance models ‘is-a’. A library has books.
Mini exercise (hard)
Build a Library that holds Book objects. Give Library an add(title) method, a borrow(title) method that marks a matching available book as borrowed (and returns True, or False if it can’t), and an available() method that returns the titles still on the shelf. Add two books, borrow one, and print what’s available.
Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.
class Book:
def __init__(self, title):
self.title = title
self.borrowed = False
class Library:
def __init__(self):
self.books = []
def add(self, title):
# TODO: add a new Book to self.books
pass
def borrow(self, title):
# TODO: mark the first matching available book borrowed, return True; else False
return False
def available(self):
# TODO: return a list of titles that are not borrowed
return []
lib = Library()
lib.add("Python 101")
lib.add("Clean Code")
lib.borrow("Python 101")
print(lib.available())
Give Book a borrowed flag starting at False. In borrow, loop to find the first matching, un-borrowed book, flip its flag, and return True; return False if none match.
class Book:
def __init__(self, title):
self.title = title
self.borrowed = False
class Library:
def __init__(self):
self.books = []
def add(self, title):
self.books.append(Book(title))
def borrow(self, title):
for book in self.books:
if book.title == title and not book.borrowed:
book.borrowed = True
return True
return False
def available(self):
return [book.title for book in self.books if not book.borrowed]
lib = Library()
lib.add("Python 101")
lib.add("Clean Code")
lib.borrow("Python 101")
print(lib.available())
['Clean Code']
assert len(lib.books) == 2, "add() should create a Book for each title"
assert all(isinstance(b, Book) for b in lib.books), "Library.books should hold Book objects"
assert lib.available() == ["Clean Code"], "available() should list only the un-borrowed titles"
assert lib.borrow("Python 101") is False, "a book already borrowed cannot be borrowed again"
assert lib.borrow("Clean Code") is True, "an available book can be borrowed"
assert lib.available() == [], "after borrowing both, none are available"
print("✓ A working object model!")