From Scripting to Engineering: Mastering OOP in Python
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.