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.
Encapsulation
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.
Inheritance
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.
Separation of Concerns
Product knows about a single item. LechonBusiness knows about the inventory and revenue. Each layer does exactly one job and does it well.
@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.
The Product Base Class
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.
_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.The Product Subclasses
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.
The LechonBusiness Class
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.
The sell_product() Method
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 = 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.The Main Execution Block
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) # 5 kilos business.sell_product("Whole Roasted Chicken", 3) # 3 whole business.sell_product("Tuna Panga", 8) # 8 kilos print(f"\nTotal Revenue: โฑ{business.total_revenue:,.2f}")
โ Why used
The if __name__ == "__main__" guard ensures this block only runs when the script is executed directly โ not when it is imported as a module. This is the standard Python entry-point pattern.
โก Significance
The expected total revenue from this run is (5ร750) + (3ร280) + (8ร350) = โฑ7,390.00. The read-only total_revenue property means the business always reports accurate totals โ no external code can tamper with it.
โข How it operates
Stock is set up first via add_product_stock(), then show_inventory() prints all products, then each sell_product() call validates, deducts stock, and accumulates revenue in sequence.
Live Order Form
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 Cheat Sheet
Glossary
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.Why OOP Matters in the Real World
Understanding OOP is not just about syntax โ it is about how professional developers think when building software that scales, evolves, and survives production environments. The lechon sales system is a microcosm of every serious Python application.
๐ง Maintainability
When prices change at Mang Juan's, you update exactly one line โ the constructor argument in PorkLechon. OOP code has a single source of truth for each piece of data, which drastically reduces bugs introduced by inconsistent manual updates across multiple files.
๐งช Testability
Because Product encapsulates its state and validates input, writing unit tests is straightforward. You can test price setter rejection independently without spinning up an entire sales system โ each class is a self-contained unit with predictable behavior.
๐ฆ Reusability
Adding a new product โ say, BeefCaldereta โ requires only a three-line subclass. All the business logic in LechonBusiness works immediately because the new class inherits the same interface. This is the power of the Liskov Substitution Principle in action.
๐ Scalability
This same OOP architecture powers enterprise systems. Django models, SQLAlchemy entities, FastAPI Pydantic schemas โ all use the same class-based design. Mastering OOP at the lechon level means your mental model transfers directly to production-grade Python frameworks.
Product base class mirrors an abstract BaseModel; a LechonBusiness mirrors a service layer. You are learning the right vocabulary.Convention vs. Enforcement: The Single Underscore Rule
Python uses _attr (single underscore) as a convention for "protected" attributes โ other programmers should not access them directly, but Python does not actually prevent it. Double underscore __attr triggers name-mangling, making access from outside the class genuinely difficult. This system exemplifies Python's philosophy: "We're all consenting adults here."
Type Hints Are Not Enforced โ But They Are Essential
The name: str, price: float, and -> None annotations in this code are not enforced by Python at runtime. They exist for readability, IDE autocomplete, and static analysis tools like mypy. In professional codebases, type hints are treated as first-class documentation โ and tools like pyright will flag violations in CI pipelines before code ever reaches production.
Why float for Quantity in sell_product()?
Pork lechon and tuna panga are sold by the kilo, so the quantity can be fractional โ e.g., 2.5 kilos. Using float for quantity in sell_product() rather than int handles this gracefully. In a production system you might use Python's decimal.Decimal to avoid floating-point precision issues in financial calculations.
OOP Use Cases Beyond Lechon
Once you understand the ProductโLechonBusiness pattern, you can apply it to virtually any domain. The same encapsulation and inheritance structure solves radically different problems.
Banking System
A BankAccount base class with SavingsAccount and CheckingAccount subclasses. Property setters prevent negative balances; a Bank composition class manages all accounts and total assets.
E-commerce Store
A Product base class with PhysicalProduct, DigitalProduct subclasses overriding a deliver() method differently. A ShoppingCart composes products exactly like LechonBusiness does.
Game Development
A Character base class with Warrior, Mage, Rogue subclasses inheriting health and attack(). Properties guard HP against invalid values. A GameWorld composition manages all entities.
Hospital System
A Patient base class extended by InPatient and OutPatient subclasses. Validated setters prevent negative medication doses. A Hospital class holds all patients and computes total billing.
Library Management
A LibraryItem base with Book, Magazine, DVD subclasses. Properties control checkout status. A Library composition class manages inventory and overdue tracking.
Vehicle Fleet
A Vehicle base with Car, Truck, Motorcycle subclasses each overriding fuel_cost(). A Fleet composition class tracks total mileage, maintenance, and revenue per vehicle.
Python OOP Learning Roadmap
This tutorial covers the core of OOP. Here is where to go next to build genuine professional Python expertise.
Classes & Instances
Master __init__, self, and the difference between class attributes and instance attributes. Build 5โ10 simple classes from scratch without inheritance.
Properties & Validation
Learn @property, @x.setter, and @x.deleter. Practice writing setters that raise ValueError or TypeError for invalid input.
Subclasses & super()
Build class hierarchies with 2โ3 levels. Understand method resolution order (MRO) and when to call super() vs. override completely.
Dunder Methods
Go beyond __init__ and __str__. Learn __repr__, __eq__, __lt__, __add__, and __len__ to make classes behave like built-in types.
Abstract Classes & Interfaces
Use abc.ABC and @abstractmethod to enforce method implementation in subclasses. Learn the difference between abstract classes, interfaces, and mixins.
OOP in Django & FastAPI
Apply your OOP knowledge to real frameworks. Django models are classes with inherited save/query methods. FastAPI Pydantic models use @validator โ your property pattern, production-hardened.
Comments & Discussion
@propertyclick for me. I kept wondering why not just use getters like in Java, but the clean public API argument makes total sense. The lechon example is so relatable โ much better than generic "Animal" class examples.LechonBusinesshas products, it is not a product โ that sentence alone saved me from a design mistake in my current project.floatfor quantity insell_productinstead ofint? I tried to change it to int and got errors on the 2.5 kilo orders. Now I understand โ lechon is sold by weight, not count. Real-world constraints driving design decisions. Love it.Discountclass using composition, and perhaps areport_summary()method that formats revenue per product. Also curious how you would handle the case where the same product exists in two branches of the business.