Python OOP —
Lechon Sales System
Full Walkthrough
A complete walkthrough of a Python OOP Lechon Sales management system — every class, every method, every price. Extended with OOP theory, a live order form, a cheat sheet, and a glossary. Perfect for beginners and intermediate coders.
What is Object-Oriented Programming?
Before dissecting the code, it helps to understand the thinking behind OOP. Every line in this program was shaped by the same principles that define how professional Python code is structured.
Each class bundles its data and the methods that operate on it. Product guards its price and quantity through validated property setters — external code cannot corrupt them directly.
PorkLechon, ChickenLechon, and TunaPanga all inherit from Product. They reuse the parent's logic and simply provide their own name and price — no duplicated code.
Product knows about a single item. LechonBusiness knows about the inventory and revenue. Each layer does exactly one job and does it well.
Why Python properties? The @property decorator lets you expose attributes as if they were plain variables while secretly running validation logic. External code writes product.price = 500 and the setter silently checks it is positive — clean public API, safe internals.
Define the blueprint — class Product
A class is a template. Product describes what every item in the business holds: a name, price, and quantity. Each specific product (pork, chicken, tuna) is an instance stamped from that template.
Extend with subclasses — PorkLechon(Product)
Writing class PorkLechon(Product) means PorkLechon is a Product. It calls super().__init__() to run the parent's setup with the correct name and price — inheriting all getters, setters, and __str__ for free.
Compose into a business — LechonBusiness
LechonBusiness holds a list of Product objects. One class containing instances of another is called composition — the standard way to build complex systems from small, tested pieces.
Guard data with validated setters
Setting product.price = -100 calls the @price.setter, which raises ValueError. This is encapsulation in action — the class protects its own data integrity, even against programmer mistakes.
lechon_sales.py — Complete
Here's the complete program. Every line is explained in the sections below. Copy it, run it, and modify it — this is your code now.
class Product: """Base class for all products sold in the lechon business.""" def __init__(self, name: str, price: float, quantity: int = 0): self._name = name # protected attribute self._price = price # protected attribute self._quantity = quantity # protected attribute @property def name(self) -> str: return self._name @property def price(self) -> float: return self._price @price.setter def price(self, new_price: float): if new_price <= 0: raise ValueError("Price must be positive") self._price = new_price @property def quantity(self) -> int: return self._quantity @quantity.setter def quantity(self, new_quantity: int): if new_quantity < 0: raise ValueError("Quantity cannot be negative") self._quantity = new_quantity def __str__(self) -> str: return f"{self._name} - ₱{self._price:.2f} (Stock: {self._quantity})" class PorkLechon(Product): def __init__(self, quantity: int = 0): super().__init__("Pork Lechon", 750.00, quantity) class ChickenLechon(Product): def __init__(self, quantity: int = 0): super().__init__("Whole Roasted Chicken", 280.00, quantity) class TunaPanga(Product): def __init__(self, quantity: int = 0): super().__init__("Tuna Panga", 350.00, quantity) class LechonBusiness: def __init__(self, business_name: str): self._business_name = business_name self._products = [] # list of Product objects self._total_revenue = 0.0 # private attribute def add_product_stock(self, product: Product) -> None: for p in self._products: if p.name == product.name: p.quantity += product.quantity return self._products.append(product) def sell_product(self, product_name: str, quantity: float) -> None: for p in self._products: if p.name.lower() == product_name.lower(): if p.quantity >= quantity: p.quantity -= quantity revenue = quantity * p.price self._total_revenue += revenue print(f"Sold {quantity} {p.name}. Revenue: ₱{revenue:.2f}") return else: print(f"Not enough stock for {p.name}.") return print(f"Product '{product_name}' not found.") def show_inventory(self) -> None: print(f"\n--- {self._business_name} Inventory ---") for p in self._products: print(p) print("----------------------------------\n") @property def total_revenue(self) -> float: return self._total_revenue @property def business_name(self) -> str: return self._business_name if __name__ == "__main__": business = LechonBusiness("Mang Juan's Lechon House") business.add_product_stock(PorkLechon(50)) business.add_product_stock(ChickenLechon(20)) business.add_product_stock(TunaPanga(30)) business.show_inventory() business.sell_product("Pork Lechon", 5) business.sell_product("Whole Roasted Chicken", 3) business.sell_product("Tuna Panga", 8) print(f"\nTotal Revenue: ₱{business.total_revenue:,.2f}")
Product — The Blueprint
class Product: def __init__(self, name, price, quantity=0): self._name = name # protected — leading underscore by convention self._price = price self._quantity = quantity
① Why used
The single underscore prefix on _name, _price, _quantity signals to other developers: "don't touch these directly." Access is controlled through properties, giving the class full authority over its own data.
② Significance
Making Product a base class means every product in the system — pork, chicken, tuna — shares the same interface. Code that works with a Product works with any of its subclasses automatically.
③ How it operates
When you call PorkLechon(10), Python runs Product.__init__ via super(), storing "Pork Lechon", 750.00, and 10 as protected attributes on the new instance.
⚠ Protected vs Private in Python: One underscore _attr = protected by convention (accessible but don't). Two underscores __attr = name-mangled by Python to prevent accidental override. This code uses one underscore, meaning the convention is enforced by discipline, not by the language.
Inheritance in Action
class PorkLechon(Product): def __init__(self, quantity=0): super().__init__("Pork Lechon", 750.00, quantity) class ChickenLechon(Product): def __init__(self, quantity=0): super().__init__("Whole Roasted Chicken", 280.00, quantity) class TunaPanga(Product): def __init__(self, quantity=0): super().__init__("Tuna Panga", 350.00, quantity)
| Code | Product | Unit | Price |
|---|---|---|---|
| 1 | Pork Lechon | per kilo | ₱750.00 |
| 2 | Whole Roasted Chicken | per whole | ₱280.00 |
| 3 | Tuna Panga | per kilo | ₱350.00 |
① Why used
Each subclass represents a real product with a fixed, known price. Encoding price at the class level means there's one authoritative source — change PorkLechon once and it's updated everywhere.
② Significance
super().__init__(...) delegates initialization to the parent. The subclass only passes its specific values; all the property validation, __str__ formatting, and getter/setter logic is inherited automatically.
③ How it operates
Calling TunaPanga(30) triggers Product.__init__("Tuna Panga", 350.00, 30). The resulting object has .name, .price, .quantity, and __str__ all working immediately — zero extra code.
LechonBusiness — The Manager
class LechonBusiness: def __init__(self, business_name): self._business_name = business_name self._products = [] # composition: list of Product objects self._total_revenue = 0.0 def add_product_stock(self, product): for p in self._products: if p.name == product.name: p.quantity += product.quantity # merge stock return self._products.append(product) # new product
① Why used
LechonBusiness is the container that manages the full inventory. Separating business logic from product logic keeps each class focused on one responsibility — a core principle of good OOP design.
② Significance
_total_revenue is read-only via @property total_revenue — external code can read it but never accidentally overwrite it. This is encapsulation protecting business-critical data.
③ How it operates
add_product_stock() searches existing products by name. If found, it adds to stock (prevents duplicates). If not, it appends the new product. This makes re-stocking and first-time adding work with the same method call.
Guard Clauses · Stock Deduction · Revenue
def sell_product(self, product_name, quantity): for p in self._products: if p.name.lower() == product_name.lower(): if p.quantity >= quantity: # Guard — enough stock? p.quantity -= quantity revenue = quantity * p.price self._total_revenue += revenue print(f"Sold {quantity} {p.name}. Revenue: ₱{revenue:.2f}") return else: print(f"Not enough stock for {p.name}.") # Guard fail return print(f"Product '{product_name}' not found.")
① Why used
Validation ensures no revenue is counted and no stock deducted unless the product exists and has sufficient quantity. Both guards exit early with a clear message, keeping the success path flat and readable.
② Significance
Case-insensitive matching (.lower()) means "pork lechon", "PORK LECHON", and "Pork Lechon" all find the same product — matching how a real cashier would enter a name.
③ How it operates
On success: p.quantity -= quantity calls the quantity setter (which validates ≥ 0), then revenue = quantity * p.price computes the total and accumulates it into the private _total_revenue.
💡 Revenue formula: revenue = quantity * p.price — for kilo-based products this is kilos × price per kilo. For chicken it is count × ₱280. The same formula works for both because the unit is embedded in the price definition of each subclass.
Place an Order — Interactive Demo
The interactive implementation of the program above. Select a product, enter the quantity, and confirm to compute the total cost.
✅ Order Receipt
Thank you for your purchase — Mang Juan's Lechon House 🇵🇭
OOP Quick Reference
Every syntax pattern and built-in used in this program, with a concise definition.
Key Terms
Every important term used in this guide, defined precisely in the context of Python OOP.
Product is the blueprint; PorkLechon(10) is an instance built from it.self as its first argument so it can read and modify its own attributes.PorkLechon inherits __str__, getters, and setters from Product.@x.setter it lets you add validation logic that runs transparently on every write.super().__init__(...) runs the parent's constructor, letting the subclass reuse initialization logic without duplicating it.return or raise that exits a function immediately if a precondition fails, keeping the main success logic flat and readable rather than deeply nested.LechonBusiness holds a list of Product objects — it is composed of products, not a subclass of them.Stay in the Loop
Level up your Python knowledge.
In-depth Python tutorials, OOP deep-dives, and software engineering guides — delivered straight to your inbox. No spam, ever.
🔒 Join 8,400+ developers. Your email is never shared or sold.