These exercises combine everything from the OOP lessons. Try each one before reading the hints, then compare with the reference solution. Each comes with test code you can paste in to check your work.
| # | Exercise | Concepts practiced |
|---|---|---|
| 1 | Library system | __eq__, __str__, __contains__, __len__, read-only property |
| 2 | Animal kingdom | Abstract classes, overriding, __lt__, __add__ |
| 3 | Bank | Encapsulation, class variables, computed values, Singleton |
Exercise 1: Library system#
Build a library where:
- A book has a title, an author and a unique ID.
- Two books are equal if they have the same ID.
- Printing a book shows meaningful information.
- A library holds a collection of books. You can add a book, check whether a book is in the library, and get the number of books.
- The library's name can't be changed from outside.
Hints
__eq__onBookcompares IDs;__str__formats the output.incalls__contains__,len()calls__len__.- Store the name as
self.__nameand expose a getter-only@property.
Reference solution
class Book:
def __init__(self, title, author, book_id):
self.title = title
self.author = author
self.book_id = book_id
def __eq__(self, other):
return isinstance(other, Book) and self.book_id == other.book_id
def __str__(self):
return f"Title: {self.title} | Author: {self.author}"
class Library:
def __init__(self, name="City Library"):
self.__name = name
self.books = []
@property
def name(self): # no setter, so it's read-only
return self.__name
def add_book(self, book):
self.books.append(book)
def __contains__(self, book):
return book in self.books # list membership uses Book.__eq__
def __len__(self):
return len(self.books)
Tests
book1 = Book("ABC", "XYZ", "123")
book2 = Book("ABC", "XYZ", "1234")
book3 = Book("ABC", "XYZ", "999")
print(book1 == book2) # False: different IDs
print(book1) # Title: ABC | Author: XYZ
lib = Library()
lib.add_book(book1)
lib.add_book(book2)
print(book1 in lib) # True
print(book3 in lib) # False
print(len(lib)) # 2
# lib.name = "Other" # AttributeError
Exercise 2: Animal kingdom#
Build an animal system where:
- Every animal has a name and can speak and move, but each kind does it differently.
- Nobody can create a generic animal directly.
- A dog barks and runs; a bird chirps and flies.
- A list of animals can be sorted alphabetically by name.
- Two animals of the same type can be added together to make a new animal whose name combines both.
Hints
- Make
AnimalanABCwith abstractspeak()andmove(). list.sort()uses__lt__; define it once onAnimal.__add__on each subclass returns a new instance of that subclass.type(self)(...)works generically.
Reference solution
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def speak(self):
pass
@abstractmethod
def move(self):
pass
def __lt__(self, other):
return self.name < other.name
def __add__(self, other):
if type(self) is not type(other):
return NotImplemented # only merge animals of the same kind
return type(self)(self.name + other.name)
class Dog(Animal):
def speak(self):
print("Woof!")
def move(self):
print("Running")
class Bird(Animal):
def speak(self):
print("Chirp!")
def move(self):
print("Flying")
Tests
try:
Animal("Generic")
print("FAIL: Animal should be abstract")
except TypeError:
print("PASS: can't instantiate Animal")
Dog("Bruno").speak() # Woof!
Bird("Tweety").move() # Flying
animals = [Dog("Zeus"), Bird("Aarav"), Dog("Bruno"), Bird("Mango")]
animals.sort()
print([a.name for a in animals]) # ['Aarav', 'Bruno', 'Mango', 'Zeus']
print((Dog("Bruno") + Dog("Max")).name) # BrunoMax
Exercise 3: Bank#
Build a banking system that meets these requirements.
Accounts
- An account has an owner, a balance and an account number.
- The balance can't be set directly; it changes only through
depositandwithdraw. - Withdrawing more than the balance is not allowed.
- Two accounts are equal if they have the same account number.
- Printing an account shows meaningful information.
Money
- The total money across all accounts is available without looping over accounts.
- Interest can be calculated from the current balance and a rate, without storing it.
The bank
- Only one bank can ever exist. The bank holds accounts; you can add one and look one up by number.
Hints
- Private
__balance+ getter-only@propertyblocks direct assignment. - A class variable (
BankAccount.total_money) updated in__init__,depositandwithdrawgives the running total. - Interest is a method (or property) computed on demand.
- Override
__new__for the Singleton, and guard__init__so a secondBank()doesn't wipe the accounts.
Reference solution
class BankAccount:
total_money = 0 # shared across all accounts
def __init__(self, owner, balance, acc_num):
self.owner = owner
self.acc_num = acc_num
self.__balance = balance
BankAccount.total_money += balance
def __eq__(self, other):
return (isinstance(other, BankAccount)
and self.acc_num == other.acc_num)
def __str__(self):
return (f"Owner: {self.owner} | Balance: {self.__balance} "
f"| Account: {self.acc_num}")
@property
def balance(self):
return self.__balance
def deposit(self, amount):
if amount <= 0:
print("Invalid deposit")
return
self.__balance += amount
BankAccount.total_money += amount
def withdraw(self, amount):
if amount <= 0 or amount > self.__balance:
print("Invalid withdrawal")
return
self.__balance -= amount
BankAccount.total_money -= amount
def interest(self, rate_percent):
return self.__balance * rate_percent / 100
class Bank:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# __init__ runs on every Bank() call,
# so only set up state the first time
if not hasattr(self, "accounts"):
self.accounts = {}
def add_account(self, account):
self.accounts[account.acc_num] = account
def get_account(self, acc_num):
return self.accounts.get(acc_num)
Tests
acc1 = BankAccount("Bruno", 1000, "ACC001")
try:
acc1.balance = 9999
print("FAIL: balance should be read-only")
except AttributeError:
print("PASS: balance is protected")
acc1.deposit(500)
acc1.withdraw(200)
acc1.withdraw(99999) # Invalid withdrawal
print(acc1.balance) # 1300
acc2 = BankAccount("Max", 2000, "ACC002")
acc3 = BankAccount("Charlie", 1000, "ACC001")
print(acc1 == acc2, acc1 == acc3) # False True
print(acc1)
print(BankAccount.total_money) # 1300 + 2000 + 1000 = 4300
print(acc1.interest(10)) # 130.0
bank1, bank2 = Bank(), Bank()
print(bank1 is bank2) # True
bank1.add_account(acc1)
bank1.add_account(acc2)
print(bank2.get_account("ACC002")) # same bank, so acc2 is found
Key takeaways#
- Dunder methods make your classes work with
==,in,len(),sort()and+. - Abstract base classes stop callers creating incomplete objects.
- Private attributes + getter-only properties give read-only state.
- Class variables track totals across all instances.
- A
__new__-based Singleton needs an__init__guard, because__init__runs on every call.