DEV Community

Cover image for From Scripting to Engineering: Mastering OOP in Python
Dirghantara, ST I Putu
Dirghantara, ST I Putu

Posted on

From Scripting to Engineering: Mastering OOP in Python

πŸš€ Most developers start by writing Procedural code. It’s like a Recipe: a list of instructions followed line-by-line. It’s fine for a 10-line script, but in a large system, it becomes a mess β€” one change here breaks ten things there. To truly understand Object-Oriented Programming (OOP), you have to stop thinking about β€œcode” and start thinking about the Real World.

In the real world, everything is an Object. Your phone, your car, even your pet cat. Every object has two things:

What it is (Data / Attributes): Like color, weight, or brand.
What it does (Behavior / Methods): Like making a call, driving, or meowing.. It’s like building a Team: specialized objects that manage their own data and logic.

Here is the breakdown of the 4 Pillars of OOP using a real-world Shopping App.

πŸ“¦ 1. Encapsulation

The Concept: Bundling data and methods into a single unit and β€œhiding” the messy internal workings.

Real World Analogy: Imagine your business cash. In Procedural, the money is scattered on the table. In OOP, its in a Safe Box. You dont touch the money directly; you use the keypad (methods) to interact with it.

Procedural: Data is naked. Anyone can modify the cart_items list, leading to bugs.

# --- THE PROCEDURAL APPROACH ---
cart_items = []
def add_item(name, price):
    cart_items.append({'name': name, 'price': price})
def get_total():
    return sum(item['price'] for item in cart_items)
# Implementation
add_item("Mechanical Keyboard", 150.00)
add_item("Gaming Mouse", 80.00)
cart_items = [] # Oops! Someone reset the global variable accidentally.
print(f"Total: ${get_total()}") # Output: 0 (Data lost!)
Enter fullscreen mode Exit fullscreen mode

OOP: We use Private Attributes (double underscores ) to protect the data.

# --- THE OOP APPROACH ---
class ShoppingCart:
    def __init__(self):
        # Private attribute: cannot be accessed directly from outside
        self.__cart_items = [] 

    def add_item(self, name: str, price: float):
        self.__cart_items.append({"name": name, "price": price})

    def get_total(self):
        return sum(item['price'] for item in self.__cart_items)

# --- IMPLEMENTATION ---
cart = ShoppingCart()
cart.add_item("Mechanical Keyboard", 150.00)
cart.add_item("Gaming Mouse", 80.00)

# cart.__cart_items = []  # ERROR! This is protected.
print(f"Total: ${cart.get_total()}") # Output: Total: $230.0
Enter fullscreen mode Exit fullscreen mode

The Win: You control how data is modified.

🎭 2. Abstraction & Polymorphism

The Concept: Hiding the β€œhow” and only showing the β€œwhat.”

Real World Analogy: Think of a Universal Remote. You press Power, and the TV, Soundbar, and DVD player all turn on. You dont care how each device powers up; you just care that they all follow the same command.

Procedural: A massive if-else block checking every discount type. Adding a new discount means editing (and potentially breaking) the main code. Plus, your functions now have a β€œParameter Explosion” β€” too many arguments to track, making the code a fragile mess. Imagine if the apply_discount() function is implemented in a hundred different files; you would have to edit it one by one.

# --- THE PROCEDURAL APPROACH ---
# Too many arguments to keep track of!
def apply_discount(total, type, value, voucher_code, voucher_exp, buy_X, get_Y):
    # A massive if-else block    
    if type == "percentage":
        return total * (1 - value/100)
    elif type == "fixed":
        return total - value
    # To add a "Voucher" discount, you must change this code... again.
    # To add a "buyXgetY" discount, you must change this code... again.

# Implementation
print(apply_discount(100, "percentage", 10, '', '', 0, 0)) # 90.0
print(apply_discount(100, "buyXgetY", 10, '', 0)) # error, missing argument
Enter fullscreen mode Exit fullscreen mode

OOP: Use an Abstract Base Class (ABC) to define a contract.

# --- THE OOP APPROACH ---
from abc import ABC, abstractmethod

# --- ABSTRACTION: The Blueprint ---
class Discount(ABC):
    @abstractmethod
    def apply(self, total: float) -> float:
        pass

# --- POLYMORPHISM: Flexible Implementations ---

class PercentageDiscount(Discount):
    def __init__(self, percent: int):
        self.percent = percent

    def apply(self, total): 
        return total * (1 - self.percent / 100)

class VoucherDiscount(Discount):
    def __init__(self, code: str, amount: float):
        self.code = code
        self.amount = amount

    def apply(self, total):
        print(f"--- Applying Voucher: {self.code} ---")
        return total - self.amount

class BuyXGetYDiscount(Discount):
    def __init__(self, x_qty: int, y_qty: int, unit_price: float):
        self.x_qty = x_qty
        self.y_qty = y_qty
        self.unit_price = unit_price

    def apply(self, total):
        # Example logic: Get Y items free for every X items bought
        free_items = self.y_qty 
        discount_value = free_items * self.unit_price
        print(f"--- Buy {self.x_qty} Get {self.y_qty} Applied ---")
        return total - discount_value

# --- IMPLEMENTATION (The code that NEVER changes) ---
def process_checkout(total, discount_type: Discount):
    # This remains simple no matter how complex the discount logic gets
    final_price = discount_type.apply(total)
    print(f"Final Price after discount: ${final_price}\n")

# Usage
process_checkout(200, PercentageDiscount(15))          # 15% off
process_checkout(200, VoucherDiscount("SAVE50", 50.0))  # $50 voucher
process_checkout(200, BuyXGetYDiscount(2, 1, 20.0))     # Buy 2 get 1 ($20) free
Enter fullscreen mode Exit fullscreen mode

The Win: This is the Open-Closed Principle. Your code is open for extension (adding new discounts) but closed for modification (you never have to change process_checkout() again).

🧬 3. Inheritance

The Concept: Creating a new class based on an existing one so you don’t have to start from scratch.

Real World Analogy: Think of the β€œBase Appliance.” Every appliance needs a power cord and an On/Off switch.

A Toaster inherits the power cord but adds heating coils.
A Blender inherits the power cord but adds spinning blades.
Why? You don’t β€œre-invent” electricity and plugs every time you build a new kitchen tool. You just inherit the basics and add your own unique features.

Procedural: The β€œCopy-Paste” Nightmare. In procedural, every time you add a new way to process a payment, you copy the old logic and add more parameters (card_number, cvv, email, etc.).”

# --- THE PROCEDURAL APPROACH ---
def process_credit_card(user, amount, card_number, cvv):
    # Repeated logic for every payment
    if not user.get("is_logged_in", False):
        return {"status": "failed", "reason": "User not logged in"}
    if amount <= 0:
        return {"status": "failed", "reason": "Cart total must be > zero"}

    status = "success" if len(card_number) == 16 and len(cvv) == 3 else "failed"
    return {"type": "credit_card", "amount": amount, "status": status}

def process_paypal(user, amount, email):
    # Same repeated checks
    if not user.get("is_logged_in", False):
        return {"status": "failed", "reason": "User not logged in"}
    if amount <= 0:
        return {"status": "failed", "reason": "Cart total must be > zero"}

    status = "success" if "@" in email else "failed"
    return {"type": "paypal", "amount": amount, "status": status}
Enter fullscreen mode Exit fullscreen mode

OOP: Use Inheritance to reuse code. We put the shared logic in the Parent and use Method Overriding in the Child to change how the payment is process.

# --- THE OOP APPROACH ---
from abc import ABC, abstractmethod
from typing import Dict
# --- PARENT CLASS ---
class Payment(ABC):
    def __init__(self, user: dict, amount: float):
        self.user = user
        self.amount = amount
        self.status = "pending"
        self.reason = None
    def _pre_check(self) -> bool:
        """Shared validation logic for all payments"""
        if not self.user.get("is_logged_in", False):
            self.status = "failed"
            self.reason = "User not logged in"
            return False
        if self.amount <= 0:
            self.status = "failed"
            self.reason = "Cart total must be greater than zero"
            return False
        return True
    @abstractmethod
    def validate(self) -> bool:
        """Child implements payment-specific validation"""
        pass
    def process(self) -> Dict[str, str]:
        if not self._pre_check():
            return {"type": self.__class__.__name__, "amount": self.amount,
                    "status": self.status, "reason": self.reason}
        self.status = "success" if self.validate() else "failed"
        return {"type": self.__class__.__name__, "amount": self.amount,
                "status": self.status}
# --- CHILD CLASSES ---
class CreditCardPayment(Payment):
    def __init__(self, user: dict, amount: float, card_number: str, cvv: str):
        super().__init__(user, amount)
        self.card_number = card_number
        self.cvv = cvv
    def validate(self) -> bool:
        return len(self.card_number) == 16 and len(self.cvv) == 3
class PayPalPayment(Payment):
    def __init__(self, user: dict, amount: float, email: str):
        super().__init__(user, amount)
        self.email = email
    def validate(self) -> bool:
        return "@" in self.email
# --- USAGE ---
user = {"name": "Alice", "is_logged_in": True}
payments = [
    CreditCardPayment(user, 100, "4111111111111111", "123"),
    PayPalPayment(user, 0, "alice@example.com")  # Total=0 triggers pre-check
]
results = [p.process() for p in payments]
# results will show status, including failed pre-checks
Enter fullscreen mode Exit fullscreen mode

The Win: Dont Repeat Yourself (DRY). If you find a bug in finalize(), you fix it once in the class, and its fixed everywhere.

βš–οΈ Conclusion

Object-Oriented Programming (OOP) is more than just a coding style β€” it’s a way to model real-world problems in software. By focusing on objects with data (attributes) and behavior (methods), OOP allows developers to:

  • Encapsulate data and control access, preventing accidental modification.
  • Abstract complex logic behind clear interfaces, exposing only what’s necessary.
  • Reuse and extend code through inheritance, reducing duplication and enforcing consistency.
  • Implement polymorphism, enabling flexible, interchangeable components that follow the same contract.

Compared to procedural programming, OOP improves maintainability, scalability, and readability, making it easier to manage large systems, add new features, and enforce consistent business rules. Ultimately, mastering OOP empowers developers to build robust, flexible, and future-proof software that mirrors the complexity of the real world.

Top comments (0)