Learning Goals
3 minBy the end of this lesson you can:
- Decide what belongs in a class and what belongs in the program.
- Write a class whose job is to look after a list of other objects.
- Combine
__init__, methods, a list of objects and inheritance in one file. - Judge someone else's class and say exactly what is wrong with it.
Warm-Up · Whose Job Is It?
5 minYou are building stock control for a shop runcit. For each job, decide where it belongs: on one item, on the whole shop, or in the program that talks to the shopkeeper.
- Work out what 12 bags of rice are worth.
- Ask the shopkeeper which item to sell.
- Find the item called "Pancake".
- Refuse to sell more than there is.
- Add up the value of everything on the shelves.
- Print the menu of choices.
Answer
- One item: its own value, and refusing to oversell itself.
- The shop: finding by name, and totalling the shelves — both need the whole list.
- The program: the two that talk to a human. Nothing else.
Ask what does this job need to see? One product — it is a method on Item. Every product — it belongs to Inventory. A person — it belongs in the program. Get that split right and the program at the bottom stays short.
New Concept · An Object That Owns a List
12 minIn OOP-04 the list of objects sat loose in your program, with helper functions beside it. That works until a second list appears and the functions must be told which one to use.
menu = [] find_item(menu, "pancake") total_value(menu)
Put the list inside a class, and it stops being something the program has to carry around:
class Inventory: """The whole shop's stock: a list of items, and the jobs done to it.""" def __init__(self, shop_name): self.shop_name = shop_name self.items = [] def add(self, item): """Put a new item on the shelves.""" self.items.append(item) def find(self, name): """Return the item with that name, or None.""" for item in self.items: if item.name.lower() == name.lower(): return item return None
Now the calls read as sentences, and the list has an owner:
shop.add(Item("Beras 5kg", 24.50, 12)) pancake = shop.find("pancake")
A fresh item is an item, so FreshItem inherits from Item (OOP-05). A shop has items, so Inventory holds them in an attribute. Both are everywhere in real code, and choosing the wrong one is the classic beginner mistake. Say the sentence out loud before you write the class.
The payoff is what disappears from your program. Nothing outside these classes ever writes item.qty = item.qty - 3, because sell owns that rule — and nothing outside ever loops over the shelves, because Inventory does.
Build It · inventory.py, Four Steps
14 minCreate a file called inventory.py. Add each piece in order and test it before moving on.
class Item: """One product on the shelves.""" def __init__(self, name, price, qty=0): self.name = name self.price = price self.qty = qty def value(self): """Return what this line of stock is worth.""" return self.price * self.qty def restock(self, amount): """Add stock. Return the new quantity.""" self.qty = self.qty + amount return self.qty def sell(self, amount=1): """Sell stock. Return True if there was enough.""" if amount > self.qty: return False self.qty = self.qty - amount return True def is_low(self, level=5): """Return True when stock has fallen to the reorder level.""" return self.qty <= level def __str__(self): return f"{self.name:<16} $ {self.price:>6.2f} x {self.qty:>3} = $ {self.value():>7.2f}"
Test: make one item, print it, sell 3, print it again, then try to sell 999 and check you get False.
Bread and eggs behave like every other product, except that they lose half their value on their last day. That is one changed rule, so it is a child class.
class FreshItem(Item): """A fresh item IS AN item, with a shelf life.""" def __init__(self, name, price, qty=0, days_left=3): super().__init__(name, price, qty) self.days_left = days_left def value(self): """Fresh stock is half price on its last day.""" if self.days_left <= 1: return super().value() * 0.5 return super().value() def __str__(self): return super().__str__() + f" [days left: {self.days_left}]"
Test: a FreshItem with days_left=1, printed beside an ordinary Item of the same price and quantity. Both overrides extend the parent — neither one retypes the sum.
class Inventory: """The whole shop's stock: a list of items, and the jobs done to it.""" def __init__(self, shop_name): self.shop_name = shop_name self.items = [] def add(self, item): """Put a new item on the shelves.""" self.items.append(item) def find(self, name): """Return the item with that name, or None.""" for item in self.items: if item.name.lower() == name.lower(): return item return None def remove(self, name): """Take an item off the shelves. Return True if it was there.""" item = self.find(name) if item is None: return False self.items.remove(item) return True def total_value(self): """Return what all the stock is worth.""" total = 0 for item in self.items: total = total + item.value() return total def low_stock(self): """Return a list of the items that need reordering.""" low = [] for item in self.items: if item.is_low(): low.append(item) return low def report(self): """Print the whole stock list and the total.""" print(f"--- {self.shop_name} ---") for item in self.items: print(item) print(f"Total stock value: $ {self.total_value():.2f}")
Test: four items added, then shop.report().
Output
--- Shop Runcit Mia --- Beras 5kg $ 24.50 x 12 = $ 294.00 Minyak cook $ 18.90 x 8 = $ 151.20 Pancake $ 3.20 x 6 = $ 9.60 [days left: 1] Telur (10) $ 6.50 x 4 = $ 26.00 [days left: 5] Total stock value: $480.80
Look at total_value. It calls item.value() without ever asking what kind of item it is, so the bread's half-price rule is applied by the bread itself. Add a third child class tomorrow and this method does not change.
Underneath the three classes, write the menu. It should hold no stock data of its own.
from toolbox import ask_number, ask_yes_no shop = Inventory("Shop Runcit Mia") shop.add(Item("Beras 5kg", 24.50, 12)) shop.add(Item("Minyak cook", 18.90, 8)) shop.add(FreshItem("Pancake", 3.20, 6, 1)) shop.add(FreshItem("Telur (10)", 6.50, 4, 5)) while True: print() print("1 Report 2 Sell 3 Restock 4 Low stock 5 Quit") choice = ask_number("Choose: ", low=1, high=5) if choice == 5: print("Goodbye!") break if choice == 1: shop.report() continue if choice == 4: for item in shop.low_stock(): print("Reorder:", item) continue item = shop.find(input("Item name: ")) if item is None: print("No such item.") elif choice == 2: if item.sell(ask_number("How many? ", low=1)): print("Sold. Stock now", item.qty) else: print("Not enough stock.") else: print("Stock now", item.restock(ask_number("How many? ", low=1)))
Run it. Sell 99 bags of rice, ask for "soup", type a word when it wants a number. Every refusal comes from a class or from the toolbox — none of it is in the menu.
toolbox.py to hand?It is the file you built in FUN-06, sitting in the same folder. If you have not got it, replace ask_number("Choose: ", low=1, high=5) with int(input("Choose: ")) for today — then notice how quickly the menu fills up with checking that used to live somewhere else.
Try It Yourself
13 minAdd two methods to Inventory: count(), returning how many item lines the shop has, and most_valuable(), returning the item object holding the most money — or None when the shelves are empty.
Hint
def most_valuable(self): """Return the item worth the most, or None if there are none.""" if len(self.items) == 0: return None best = self.items[0] for item in self.items: if item.value() > best.value(): best = item return best
Return the item, never its value — the caller can ask for that itself.
Add two more methods, and one small function beside the classes:
value_of(item)— a key function.Inventory.sorted_items()— returns the list sorted, most valuable first. (sorted(..., reverse=True).)Inventory.restock_low(target)— brings every low item up totargetand returns how many it touched.
Then add a menu option 6 that restocks everything low to 20 and prints how many items changed. The menu gets three lines longer; the classes do the work.
Mini-Challenge 🔥 · Review Someone Else's Class
8 minCocoa handed this in. It runs. Say what is wrong with it, and how you would fix each thing — there are five problems.
stock = []
class item:
def __init__(self, n, p, q):
self.n = n
self.p = p
self.q = q
def do_everything(self):
print("=" * 30)
print(self.n, "$", self.p, "x", self.q)
stock.append(self)
print("Items in shop:", len(stock))
print("=" * 30)
def sell(self, amount):
self.q = self.q - amountAnswer
class item— lower case. Every reader expectsItem, so a class stands out from a variable.n,p,q— nobody opening this file in a month can guess them. They cost nothing to spell asname,priceandqty.do_everything— the name is the review. It draws a border, describes the item, files it in the shop and counts the shop: four jobs, none usable alone. Split into__str__, and anInventory.addthat does the filing.- The global
stock— the class reaches out of itself to a list it does not own, so this file can only ever run one shop, and importing it creates that list in your program too. The list belongs to anInventoryobject (FUN-05 said the same thing about globals in functions). sellallows the impossible — nothing stops the quantity going negative, and it returns nothing, so the caller cannot tell whether the sale happened. Guard it and returnTrueorFalse.
Every one of the five is really the same question asked five ways: does this thing know only about itself? A class that reaches for a global, or does four jobs, has stopped being one thing.
Recap · The Whole Section
3 minSix lessons, one idea: describe a kind of thing once, then make as many as you need. A class is the blueprint and an object is the thing (OOP-01). __init__ gives every object its data at birth, through self (OOP-02). Methods put behaviour beside the data, so the rules cannot be dodged (OOP-03). A list of objects turns one thing into a shop, a party or a class register (OOP-04). Inheritance shares everything two classes agree about and lets the child change one thing (OOP-05). Put them together and the program at the bottom of the file becomes short enough to read in one go (OOP-06).
Vocabulary Card
- composition ("has-a")
- One object holding others in an attribute — a shop with a list of items.
- encapsulation
- Keeping data and the rules about that data inside the same class.
- single responsibility
- One class, one thing it is about.
do_everythingfails the test. - module
- Your
inventory.py— another file can nowfrom inventory import Item.
Homework
4 minFinish inventory.py and hand it in with the menu program.
- Three classes: a parent, a child that overrides at least one method with
super(), and a manager class that owns the list. - At least eight methods across them, each with a docstring.
- No
global, and no code outside the classes that reads or writes an attribute directly — the program should only call methods. - One feature of your own invention. A receipt, a price rise, a second shop, a search that matches part of a name.
Hint: a second shop is the quickest way to prove your design is right. If Inventory("Shop Leo") can exist beside the first one with no other change, nothing is hiding in a global.
Sample · the invented feature, and the proof
inventory.py · added to Inventory
def restock_low(self, target): """Bring every low item up to target. Return how many changed.""" changed = 0 for item in self.low_stock(): item.restock(target - item.qty) changed = changed + 1 return changed def receipt(self, name, qty): """Sell qty of one item and return the receipt as a string.""" item = self.find(name) if item is None: return "No such item." if not item.sell(qty): return f"Only {item.qty} x {item.name} left." cost = item.price * qty return (f"{self.shop_name}\n" f"{qty} x {item.name} @ $ {item.price:.2f}\n" f"Total: $ {cost:.2f}")
shop.py · a second shop, in six lines
from inventory import Item, FreshItem, Inventory food stall = Inventory("Food stall Leo") food stall.add(Item("Orange juice bungkus", 2.00, 30)) food stall.add(FreshItem("Layer cake", 1.20, 4, 1)) print(food stall.receipt("orange juice bungkus", 3)) print("Restocked", food stall.restock_low(20), "lines") food stall.report()
Output
Food stall Leo 3 x Orange juice bungkus @ $2.00 Total: $6.00 Restocked 1 lines --- Food stall Leo --- Orange juice bungkus $ 2.00 x 27 = $ 54.00 Layer cake $ 1.20 x 20 = $ 12.00 [days left: 1] Total stock value: $66.00
The second shop shares every class with the first and no data at all. receipt returns a string rather than printing, so the same method could later write to a file — which is exactly what Section 4 · File Handling would let you do next.