A complete walkthrough of single inheritance, method overriding, and runtime polymorphism — explained step by step through a real Filipino corn and rice agri-market application. Every class, every method call, every design decision explained.
Walk into any palengke in the Philippines and you will find stalls selling both mais (corn) and bigas (rice) side by side. Both are grains, both are priced per kilogram, and both can be purchased in varying quantities — yet corn is corn and rice is rice. They share a common nature but differ in how they present themselves. This is exactly the intuition behind CornRice.py, and it is also the intuition behind one of the most fundamental concepts in object-oriented programming: inheritance with method overriding.
In this program, a single Product base class captures everything that is common to all items for sale — a name, a price, the ability to display itself, and the ability to calculate a total for a given quantity. Two child classes, Corn and Rice, inherit all of this but override just one method — display() — to prepend their own category label. That one override is enough to enable runtime polymorphism: a loop can call product.display() on every item in a mixed list and each object produces the right output for its own type, without a single if or isinstance check in sight.
This guide walks through every line in a logical, progressive order. It explains not just what the code does but why every decision was made — from why Corn does not need its own __init__, to how calculate_total() is inherited without repetition, to exactly what happens inside the try/except block and why two exception types are caught separately.
class Child(Parent) syntax, inheriting a constructor without repeating it, overriding a specific method in a child class, runtime polymorphism through a polymorphic list, enumerate() for numbered display, try/except ValueError with a manual raise, and the if __name__ == "__main__" entry guard — all in one cohesive, runnable Filipino agri-market program.
CornRice.py is built around three classes and one main() function. The class hierarchy is intentionally simple: one parent and two children. Before diving into the individual sections, it helps to see the full picture of how these classes relate and how data flows from program startup to a printed receipt.
name and price. Provides display() for a plain formatted string and calculate_total(quantity) for the total cost. All products inherit from it.Product. Does not define __init__ — uses Product's constructor directly. Overrides only display() to prepend the [CORN VARIETY] category label.Corn exactly in structure. Overrides display() to prepend [RICE VARIETY]. Together with Corn, it demonstrates polymorphism — same method call, different label.Corn and Rice objects, displays them with enumerate(), takes user input, validates it, calculates the total, and prints a formatted receipt — all within a try/except block.class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def display(self) -> str: return f"{self.name} - ₱{self.price:.2f} per kilogram" def calculate_total(self, quantity: int) -> float: return self.price * quantity
The Product class is the root of the entire hierarchy. It answers one fundamental question: what must every item for sale have in common? The answer is three things — a display name (name), a price in Philippine Pesos (price), the ability to format itself as a readable string (display()), and the ability to calculate a purchase total for any quantity (calculate_total()). These are defined exactly once here and inherited automatically by every child class.
The type annotations (name: str, price: float) are informational — Python does not enforce them at runtime — but they serve as built-in documentation that tells any reader exactly what types are expected. The format specifier :.2f in display() ensures the price always appears with exactly two decimal places regardless of the actual float value, producing consistent output like ₱35.00 rather than ₱35.0.
| Member | Type | Purpose |
|---|---|---|
| name | str | Human-readable product name used in display and receipt output |
| price | float | Unit price in Philippine Pesos per kilogram, used in total calculation |
| display() | → str | Returns a formatted catalog string; overridden by child classes |
| calculate_total(quantity) | → float | Returns price × quantity; inherited unchanged by all children |
Product does not know what category it is — that knowledge belongs to the child classes. Putting category logic in the base class would mean every future product type must modify the base class, or that Product would need to hold a category attribute. Letting each child override display() keeps the base class clean and each child class self-contained.
class Corn(Product): def display(self) -> str: return f"[CORN VARIETY] {self.name} - ₱{self.price:.2f} per kilogram"
The class declaration Corn(Product) is the syntax for single inheritance. The parentheses hold the parent class. From this one line, Corn automatically inherits __init__, the name and price attributes, the inherited display() from Product, and calculate_total() — everything Product has. The only thing Corn does is replace display() with its own version.
Crucially, Corn does not define __init__. This is intentional and correct. When Python looks for __init__ on a Corn instance and does not find it defined in Corn, it walks up the inheritance chain and finds Product.__init__. That method runs exactly as written in Product, setting self.name and self.price on the Corn instance. This means Corn("Yellow Corn (Mais Dilaw)", 35.00) works perfectly — no code duplication required.
__init__ body and calculate_total() into both Corn and Rice. If the pricing formula ever changed — say, adding a seasonal surcharge — you would have to update it in three places and risk getting them out of sync. With inheritance, there is one single source of truth in Product that both children share automatically.
| Member | Source | Behaviour |
|---|---|---|
| __init__ | Inherited from Product | Sets self.name and self.price — no change needed |
| display() | Overridden in Corn | Prepends [CORN VARIETY] to the output string |
| calculate_total() | Inherited from Product | Returns self.price × quantity — no change needed |
class Rice(Product): def display(self) -> str: return f"[RICE VARIETY] {self.name} - ₱{self.price:.2f} per kilogram"
Rice is structurally identical to Corn. It inherits from Product, does not define __init__, and overrides only display() — this time prepending [RICE VARIETY]. This parallel structure is deliberate. When two classes differ in only one behaviour, they should be identical in structure everywhere else. Any future programmer reading the code immediately understands the pattern: the category label is the only thing that distinguishes Corn from Rice, and both live entirely in their respective display() overrides.
Corn.display() returns a string starting with [CORN VARIETY]. Rice.display() returns a string starting with [RICE VARIETY]. That is the entire difference between these two classes. Everything else — constructor, attributes, calculate_total() — is identical because both inherit it unchanged from Product. This is inheritance working exactly as intended.
[CORN VARIETY] in the catalog display.[RICE VARIETY]. products = [
Corn("Yellow Corn (Mais Dilaw)", 35.00),
Corn("White Corn (Mais Puti)", 38.00),
Corn("Sweet Corn (Mais Matamis)", 52.00),
Rice("Sinandomeng Rice", 45.00),
Rice("Dinorado Rice", 48.00),
Rice("Jasmine Rice (Aromatic)", 55.00),
Rice("Brown Rice (Pinoy Brown)", 62.00),
Rice("Red Rice (Red Malagkit)", 70.00),
]
for i, product in enumerate(products, 1):
print(f"{i:2d}. {product.display()}")
The products list holds a mix of Corn and Rice objects. Python's type system allows this because both are subclasses of Product — they share a common interface. The for loop calls product.display() on every item without ever checking what type it is. This is runtime polymorphism: Python dispatches to Corn.display() for the first three items and to Rice.display() for the last five, producing the correct prefixed output each time.
The enumerate(products, 1) call deserves attention. enumerate() wraps any iterable and yields (index, item) tuples. The second argument 1 sets the starting index, so the catalog is numbered 1 through 8 rather than 0 through 7 — exactly what a human-facing menu should show. The format specifier :2d right-aligns the number in a two-character field, so item 1 and item 10 align neatly in the terminal.
display() was not overridden, the loop would need to check the type of each item manually: if isinstance(product, Corn): print("[CORN VARIETY] ..."). Every new product category would require a new elif branch inside the loop. With polymorphism, the loop never needs to change — adding a new class with its own display() override automatically works.
try: choice = int(input("\n🔢 Enter the number of the product you want: ")) - 1 if choice < 0 or choice >= len(products): raise ValueError("Invalid product number!") selected_product = products[choice] quantity = int(input(f"📦 Enter quantity for {selected_product.name} (in kg): ")) if quantity <= 0: raise ValueError("Quantity must be greater than zero!") total_cost = selected_product.calculate_total(quantity) # ... receipt printing ... except ValueError as e: print(f"\n❌ Invalid input! {e}") print("Please run the program again and enter valid numbers.") except Exception: print("\n❌ Unexpected error. Please try again.")
The entire interactive section of the program is wrapped in a try/except block. This is the standard Python pattern for protecting user-facing input code. The block has three distinct layers of validation, each guarding a different category of invalid input.
int(input(...)) raises ValueError automatically. The program catches it in the except ValueError clause, prints a friendly message, and stops — no crash, no traceback.if choice < 0 or choice >= len(products) check catches this and manually raises ValueError("Invalid product number!"). Using raise ValueError deliberately slots this logic into the same exception handler, keeping all input-error messaging in one place.if quantity <= 0 check catches this with another explicit raise ValueError("Quantity must be greater than zero!"), printing a specific message so the user knows exactly what went wrong.except Exception clause is a safety net for truly unexpected errors — file system issues, memory problems, or anything else that is not a ValueError. It does not print the exception message (which could expose confusing technical details to users) but simply signals that something went wrong and asks the user to try again.print("\n" + "=" * 60) print("🧾 OFFICIAL RECEIPT - Pinoy AgriMart") print("=" * 60) print(f"Product : {selected_product.display()}") print(f"Quantity : {quantity} kg") print(f"Unit Price : ₱{selected_product.price:.2f}") print(f"Total Cost : ₱{total_cost:.2f}") print("=" * 60) print("Thank you for shopping with us! 🇵🇭 Mabuhay!")
The receipt calls selected_product.display() again — the same polymorphic method used in the catalog loop. This is significant: the receipt does not need to know whether the selected product is a Corn or Rice. If the user selected Sinandomeng Rice, the receipt line reads [RICE VARIETY] Sinandomeng Rice - ₱45.00 per kilogram. If they selected Sweet Corn, it reads [CORN VARIETY] Sweet Corn (Mais Matamis) - ₱52.00 per kilogram. The same one line of receipt code handles both cases through polymorphism.
The total cost is computed by selected_product.calculate_total(quantity) — the method defined once in Product and inherited by both Corn and Rice. Whether the selected item is corn or rice is completely irrelevant to the calculation. This is the DRY principle in practice: one method, zero duplication, correct results for every product type.
[RICE VARIETY] Jasmine Rice (Aromatic) - ₱55.00 per kilogram as the product line, Quantity: 3 kg, Unit Price: ₱55.00, and Total Cost: ₱165.00. The [RICE VARIETY] prefix in the product line comes from Rice.display() — polymorphism at work inside the receipt itself.
if __name__ == "__main__": main()
The if __name__ == "__main__" guard is a Python idiom that prevents main() from running when the file is imported as a module into another script. When Python runs a file directly from the terminal, it sets the special variable __name__ to the string "__main__". When the same file is imported by another script (import corn_rice), Python sets __name__ to the module's filename instead, so the condition is False and main() never runs.
This matters for reusability. If you later wanted to write a unit test that imports the Product, Corn, and Rice classes to test them in isolation, the test would import this file. Without the guard, importing the file would immediately start the interactive shopping session — which is not what a test runner expects. The guard ensures the classes are importable without side effects, making the code modular and testable.
__init__ and calculate_total() are written exactly once in Product and inherited by both children. There is no copy of these methods in Corn or Rice. If the formula changes, one edit fixes everything.Product defines what a product is. Corn describes how a corn product displays itself. Rice describes how a rice product displays itself. main() orchestrates the user experience. No class does more than its role requires.Vegetable(Product) class with its own display() override requires zero changes to main()'s loop or receipt section. Both are closed for modification.raise ValueError carries a specific message string: "Invalid product number!" and "Quantity must be greater than zero!". These messages surface via except ValueError as e, giving the user actionable feedback rather than a generic error.name: str, price: float, quantity: int, -> str, -> float. These serve as built-in documentation and enable static type checkers like mypy to catch type errors before the program runs.enumerate(products, 1) instead of a manual counter variable is the idiomatic Python way to produce numbered lists. It produces cleaner code, avoids off-by-one errors, and automatically adapts if products are added or removed.The complete, runnable source code. Save it as corn_rice.py and run it with python corn_rice.py.
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def display(self) -> str: return f"{self.name} - ₱{self.price:.2f} per kilogram" def calculate_total(self, quantity: int) -> float: return self.price * quantity class Corn(Product): def display(self) -> str: return f"[CORN VARIETY] {self.name} - ₱{self.price:.2f} per kilogram" class Rice(Product): def display(self) -> str: return f"[RICE VARIETY] {self.name} - ₱{self.price:.2f} per kilogram" def main(): print("=" * 60) print("🇵🇭 Welcome to Pinoy AgriMart - Fresh Corn & Rice Shop (Philippines)") print("We sell high-quality local corn and rice varieties!") print("All prices are in Philippine Pesos (₱) per kilogram.") print("=" * 60) print("\n📋 AVAILABLE PRODUCTS:\n") products = [ Corn("Yellow Corn (Mais Dilaw)", 35.00), Corn("White Corn (Mais Puti)", 38.00), Corn("Sweet Corn (Mais Matamis)", 52.00), Rice("Sinandomeng Rice", 45.00), Rice("Dinorado Rice", 48.00), Rice("Jasmine Rice (Aromatic)", 55.00), Rice("Brown Rice (Pinoy Brown)", 62.00), Rice("Red Rice (Red Malagkit)", 70.00), ] for i, product in enumerate(products, 1): print(f"{i:2d}. {product.display()}") print("\n" + "=" * 60) try: choice = int(input("\n🔢 Enter the number of the product you want: ")) - 1 if choice < 0 or choice >= len(products): raise ValueError("Invalid product number!") selected_product = products[choice] quantity = int(input(f"📦 Enter quantity for {selected_product.name} (in kg): ")) if quantity <= 0: raise ValueError("Quantity must be greater than zero!") total_cost = selected_product.calculate_total(quantity) print("\n" + "=" * 60) print("🧾 OFFICIAL RECEIPT - Pinoy AgriMart") print("=" * 60) print(f"Product : {selected_product.display()}") print(f"Quantity : {quantity} kg") print(f"Unit Price : ₱{selected_product.price:.2f}") print(f"Total Cost : ₱{total_cost:.2f}") print("=" * 60) print("Thank you for shopping with us! 🇵🇭 Mabuhay!") except ValueError as e: print(f"\n❌ Invalid input! {e}") print("Please run the program again and enter valid numbers.") except Exception: print("\n❌ Unexpected error. Please try again.") if __name__ == "__main__": main()
Corn and Rice inherit from a shared Product base class. Both child classes override display() to prepend a category label, while inheriting __init__ and calculate_total() unchanged. The program handles product selection, quantity input, receipt generation, and full error handling through a try/except block.__init__, Python automatically uses the parent's through the Method Resolution Order. Since Corn and Rice need exactly the same constructor parameters as Product — just a name and a price — there is no reason to rewrite that code. This is the DRY principle in action. Defining a duplicate __init__ in Corn would be noise that adds maintenance burden without adding any value.Corn and Rice override display(). When Python calls display() on a Corn instance, it finds Corn.display() first in the Method Resolution Order and uses it. The Product.display() version is skipped entirely. This is what allows each class to produce its own category-labelled output from the same method call.products list holds a mix of Corn and Rice objects. The for loop calls product.display() on each item without checking what type it is. Python dispatches to Corn.display() for Corn objects and to Rice.display() for Rice objects automatically at runtime. This is polymorphism — the same method call produces different output depending on the object's actual type, with no isinstance checks or if/elif branches in the calling code.calculate_total() is identical for every product — it always multiplies self.price by the given quantity. There is no meaningful difference between how corn and rice calculate their totals. Defining it once in Product and letting both child classes inherit it is the DRY principle applied to methods. If the pricing formula ever changes — for example, to add a bulk discount — one edit in Product applies automatically to all product types.Product, such as class Vegetable(Product), and override display() to return a string starting with [VEGETABLE]. Add instances to the products list and the catalog loop, product selection, and receipt generation all work immediately with the new type. This is the open/closed principle — the program is open for extension and closed for modification.Now that you understand single inheritance and method overriding in CornRice.py, try adding a Vegetable(Product) class with a [VEGETABLE] prefix, a Legume class for beans and peanuts, or a shopping cart that lets the user buy multiple products in one session. Each extension requires zero changes to the existing catalog loop or receipt code — that is the power of inheritance and polymorphism.
Stay connected for more Python guides, OOP tutorials, and tech content from the Philippines. Follow on your preferred platform:
Get new Python tutorials, OOP guides, and tech articles from Valleys & Bytes delivered to your inbox — no spam, ever.
🔒 No spam. Unsubscribe any time.
💬 Leave a Comment
💡 Comments are stored locally in this session. Be respectful and constructive.