From Scripting to Engineering: Mastering OOP in Python

Published: (February 13, 2026 at 05:10 AM EST)
7 min read
Source: Dev.to

Source: Dev.to

Most developers start by writing procedural code. It’s like a recipe: a list of instructions executed line‑by‑line. That works for a 10‑line script, but in a large system it quickly becomes a mess—one change here can break ten things there.

To truly understand Object‑Oriented Programming (OOP), 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) – e.g., color, weight, brand.
  • What it does (behavior / methods) – e.g., make a call, drive, meow.

It’s like building a team: specialized objects that manage their own data and logic.

Below is a breakdown of the four 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 code the money is scattered on a table. In OOP it lives in a safe box—you never touch the cash directly; you use the keypad (methods) to interact with it.

Procedural Example

# --- 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!)

OOP Example

# --- 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 attribute is protected.
print(f"Total: ${cart.get_total()}")   # Output: Total: $230.0

The Win – You control how data is modified.


🎭 2. Abstraction & Polymorphism

The Concept

Hide the how and expose only the what.

Real‑World Analogy

A universal remote: you press Power and the TV, soundbar, and DVD player all turn on. You don’t care how each device powers up; you only care that they all respond to the same command.

Procedural Example

# --- 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
    # Adding a new discount type means editing this function again.

# Implementation
print(apply_discount(100, "percentage", 10, '', '', 0, 0))   # 90.0
# The next call fails because of missing arguments:
# print(apply_discount(100, "buyXgetY", 10, '', 0))  # error

OOP Example

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

# --- ABSTRACTION: The Blueprint ---
class Discount(ABC):
    @abstractmethod
    def apply(self, total: float) -> float:
        """Return the discounted total."""
        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: Discount):
    """Calculate final price using the supplied discount strategy."""
    final_price = discount.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

The Win – This follows the Open‑Closed Principle: your code is open for extension (add new discount types) 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

# --- 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  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  zero"}

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

OOP: Inheritance + Method Overriding

# --- 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  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

The Win

Don’t Repeat Yourself (DRY). If you find a bug in validate() or _pre_check(), you fix it once in the parent class and it’s 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. Mastering OOP empowers developers to build robust, flexible, and future‑proof software that mirrors the complexity of the real world.

0 views
Back to Blog

Related posts

Read more »

Python OOP for Java Developers

Converting a Simple Java Square Class to Python Below is a step‑by‑step conversion of a basic Square class written in Java into an equivalent Python implementa...

Constructor

Constructor - The constructor name must be the same as the class name. - A constructor is a special method used to initialize instance variables when an object...

Show HN: Simple org-mode web adapter

Org Web Adapter A lightweight local web app for browsing and editing Org files. The app is implemented as a single Python server main.py plus one HTML template...