Return to Encapsulation Hub
OOP Deep Dive · Mang Juan's Lechon

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.

📅 March 7, 2026 ⏱ 25 min read 🎓 Beginner–Intermediate 🇵🇭 Philippines
Return to Encapsulation Hub

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.

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.

1

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.

2

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.

3

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.

4

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.

lechon_sales.py
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)
CodeProductUnitPrice
1Pork Lechonper kilo₱750.00
2Whole Roasted Chickenper whole₱280.00
3Tuna Pangaper 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.

🍖 Encode Your Order — Mang Juan's Lechon House
Total Cost
—
₱0.00

✅ Order Receipt

Product—
Unit Price—
Quantity—
Total Cost —

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.

Class Syntax
class Foo:define a class
class Bar(Foo):inherit from Foo
def __init__(self)constructor
super().__init__()call parent constructor
def __str__(self)string representation
Encapsulation
self._attrprotected (convention)
self.__attrprivate (name-mangled)
@propertygetter decorator
@x.settersetter with validation
raise ValueErrorreject invalid data
Prices
PorkLechon₱750.00 / kilo
ChickenLechon₱280.00 / whole
TunaPanga₱350.00 / kilo
revenueqty × price
Common Patterns
if __name__ == "__main__"entry point guard
for p in list: if ...: returnearly return / guard clause
p.name.lower()case-insensitive match
list.append(obj)add to inventory

Key Terms

Every important term used in this guide, defined precisely in the context of Python OOP.

classA blueprint that defines the attributes and methods of an object. Product is the blueprint; PorkLechon(10) is an instance built from it.
__init__The constructor method called automatically when a new object is created. It sets up the initial state of the object by assigning values to attributes.
selfA reference to the current instance. Every instance method receives self as its first argument so it can read and modify its own attributes.
inheritanceA mechanism where a subclass automatically receives all attributes and methods of its parent class. PorkLechon inherits __str__, getters, and setters from Product.
encapsulationBundling data and the methods that operate on it inside a class, and restricting direct access to the data through protected attributes and validated property setters.
@propertyA decorator that turns a method into a readable attribute. Paired with @x.setter it lets you add validation logic that runs transparently on every write.
super()Returns a proxy to the parent class. super().__init__(...) runs the parent's constructor, letting the subclass reuse initialization logic without duplicating it.
guard clauseAn early return or raise that exits a function immediately if a precondition fails, keeping the main success logic flat and readable rather than deeply nested.
compositionOne class holding instances of another as attributes. LechonBusiness holds a list of Product objects — it is composed of products, not a subclass of them.

Share This Guide

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.

Weekly Python tutorials OOP & design patterns AI & ML deep-dives Free, cancel anytime

🔒 Join 8,400+ developers. Your email is never shared or sold.

Join the Discussion

Leave a Comment

Sort by: