DEV Community

Cover image for OOPs Concepts Explained — The Way Nobody Taught You in College
Priyanka Rakshit
Priyanka Rakshit

Posted on

OOPs Concepts Explained — The Way Nobody Taught You in College

"Good code is not written. It's designed."
OOP is how you design it.


What Even Is OOP?

Before OOP, code was a long scroll of functions calling other functions — data flying around freely, anyone could modify anything, and one bug could silently corrupt everything else. We called it spaghetti code for a reason.

Object-Oriented Programming (OOP) is a programming paradigm that organizes software around objects — units that bundle related data and behavior together — rather than around loose functions and logic.

Think of it this way:

Instead of asking "what should happen?", OOP asks "who is responsible for making it happen?"


Why Does It Matter?

Problem (Without OOP) Solution (With OOP)
One change breaks everything Maintainability — isolated changes
Copy-pasting logic everywhere Reusability — base class, used everywhere
Can't work in parallel Modularity — independent, self-contained units
Hard to grow the codebase Scalability — extend via inheritance

The Four Pillars

OOP stands on four pillars. Each solves a specific design problem:

┌─────────────────┐  ┌─────────────────┐
│  Encapsulation  │  │   Abstraction   │
│  "hide the data"│  │  "hide the how" │
└─────────────────┘  └─────────────────┘
┌─────────────────┐  ┌─────────────────┐
│   Inheritance   │  │  Polymorphism   │
│ "reuse & extend"│  │ "one call, many │
│                 │  │  behaviors"    │
└─────────────────┘  └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Let's break each one down — crisp, technical, and with real code.


Pillar 1 — Encapsulation

What

Wrapping data (attributes) and the methods that act on that data into a single class, and restricting direct external access to the internal state.

Why

  • Prevents accidental or malicious modification of internal state
  • Forces all mutations to go through validated, controlled methods
  • Internal implementation can change without breaking external callers

How

Using access modifiers:

Modifier Python Syntax Accessible From
Public balance Anywhere
Protected _balance Class + subclasses (convention)
Private __balance Class only (name-mangled)

Combined with getter and setter methods that validate before mutating.

Where

  • Banking and fintech systems
  • Any domain where data integrity must be enforced before an update
  • User authentication — password should never be readable directly

Analogy

An ATM machine — you interact via buttons (deposit, withdraw, check balance). The internal cash logic, encryption, and vault mechanism? Completely hidden from you.

Example

class BankAccount:
    def __init__(self, owner):
        self.__balance = 0       #  private — no direct access
        self.owner = owner

    def deposit(self, amount):
        if amount > 0:           #  validation lives here, not outside
            self.__balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount

    def get_balance(self):       # controlled read access
        return self.__balance


acc = BankAccount("Jeremy")
acc.deposit(500)
print(acc.get_balance())         # 500

acc.__balance = 99999            #  creates a NEW unrelated attribute
print(acc.get_balance())         # still 500 — original is safe
Enter fullscreen mode Exit fullscreen mode

Key insight: Python's __ (double underscore) triggers name mangling__balance is stored internally as _BankAccount__balance. Any attempt to access acc.__balance from outside touches a completely different (or non-existent) variable.


Pillar 2 — Abstraction

What

  • Show only essential details, hide unnecessary implementation
  • Define WHAT a class must do — not HOW it does it
  • Enforced via abstract classes and interfaces
  • An abstract class cannot be instantiated directly — it is a contract, not an object

Why

  • Reduces cognitive complexity for the caller — they only need to know the interface
  • Enables the Open/Closed Principle (OCP): open for extension, closed for modification
  • New business logic (new payment provider, new notification channel) can be added without touching existing code

How

In Python, using the abc module:

from abc import ABC, abstractmethod
Enter fullscreen mode Exit fullscreen mode

Abstract methods have a signature but no body — subclasses are forced to implement them.

Where

  • Payment gateways (Stripe, Razorpay, GPay — all follow one contract)
  • API design and service layers
  • Database drivers (MySQL, PostgreSQL — same interface, different internals)
  • Anywhere you want implementation independence

Analogy

A car's steering wheel — you turn it, the car turns. Whether it's hydraulic steering or electric — you don't know, and you don't need to. The interface is stable; the internals can vary.

Example

from abc import ABC, abstractmethod

class PaymentGateway(ABC):          #  abstract — defines the contract

    @abstractmethod
    def charge(self, amt): pass     # signature only, no body

    @abstractmethod
    def refund(self, txn_id): pass


class GPay(PaymentGateway):         # concrete implementation
    def charge(self, amt):
        print(f"GPay: charging ₹{amt}")
    def refund(self, txn_id):
        print(f"GPay: refunding {txn_id}")


class Stripe(PaymentGateway):       # another implementation
    def charge(self, amt):
        print(f"Stripe: charging ₹{amt}")
    def refund(self, txn_id):
        print(f"Stripe: refunding {txn_id}")


#  caller only knows PaymentGateway — not GPay or Stripe
def process_payment(gateway: PaymentGateway, amt):
    gateway.charge(amt)             # works for ANY gateway

process_payment(GPay(), 999)        # GPay: charging ₹999
process_payment(Stripe(), 999)      # Stripe: charging ₹999

# Tomorrow you add PhonePe — zero changes to process_payment()
Enter fullscreen mode Exit fullscreen mode

Key insight: PaymentGateway() alone throws a TypeError — you cannot instantiate an abstract class. It forces every subclass to honour the contract.


Pillar 3 — Inheritance

What

A child class (subclass) acquires the state and behaviour of a parent class (superclass) — and can extend or override it as needed.

  • Child class inherits all attributes and methods of the parent
  • Can add new fields/methods
  • Can override existing methods with its own version

Why

  • Eliminates code duplication — shared logic lives in one place
  • Models real-world "is-a" relationships naturally (Manager is an Employee)
  • A bug fixed in the parent is fixed everywhere

How

class Child(Parent):              # Python
class Child extends Parent {}     # Java / JavaScript
Enter fullscreen mode Exit fullscreen mode

Child calls super() to invoke the parent's constructor, then adds its own initialization on top.

Where

  • Entity hierarchies: Employee → Manager → Director
  • UI component libraries: BaseButton → PrimaryButton → DangerButton
  • Framework base classes: Django's Model, React's Component

Analogy

A smartphone inherits from a basic phone — it can call and SMS out of the box. On top of that, it adds camera, apps, GPS. Base features reused; new ones extended.

Example

class Employee:                           #  parent / base class
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return f"{self.name} | Salary: ₹{self.salary}"


class Manager(Employee):                  #  child class
    def __init__(self, name, salary, team_size):
        super().__init__(name, salary)    #  reuse parent __init__
        self.team_size = team_size        # extend with new attribute

    def describe(self):                   #  override parent method
        base = super().describe()
        return f"{base} | Team: {self.team_size} people"


class Director(Manager):                  # grandchild — another level
    def __init__(self, name, salary, team_size, budget):
        super().__init__(name, salary, team_size)
        self.budget = budget

    def describe(self):
        base = super().describe()
        return f"{base} | Budget: ₹{self.budget}"


d = Director("Priya", 500000, 40, 10000000)
print(d.describe())
# Priya | Salary: ₹500000 | Team: 40 people | Budget: ₹10000000
Enter fullscreen mode Exit fullscreen mode

Key insight: super() is the bridge to the parent. Each level in the chain adds its own context without rewriting what came before.


Pillar 4 — Polymorphism

What

"Many forms" — one interface, many implementations.

The same method call behaves differently depending on the actual object type at runtime.

Two forms:

  • Runtime polymorphism (method overriding) — resolved at runtime via dynamic dispatch
  • Compile-time polymorphism (method overloading) — resolved at compile time (Java/C++; Python achieves this via default args)

Why

  • Write generic code that works on any subtype — no if isinstance(...) chains
  • Add new types without modifying existing calling code (OCP again)
  • The calling code becomes elegantly type-agnostic

How

Override a parent method in a subclass. The runtime uses the actual object type (dynamic dispatch) to pick the right method — not the declared type.

Where

  • Rendering engines — each Shape draws itself: circle.draw(), square.draw()
  • Notification systems — Email, SMS, Push all implement send()
  • Serializers, event handlers, plugin architectures

Analogy

The + operator2 + 3 is arithmetic addition, "hello" + " world" is string concatenation. Same symbol, completely different behaviour based on the operand type.

Example

class Notification:
    def send(self, msg): pass           # base — meant to be overridden


class Email(Notification):
    def send(self, msg):
        print(f" Email   → {msg}")

class SMS(Notification):
    def send(self, msg):
        print(f" SMS     → {msg}")

class PushAlert(Notification):
    def send(self, msg):
        print(f" Push    → {msg}")


#  one loop, one call — works for any Notification subtype
channels = [Email(), SMS(), PushAlert()]

for channel in channels:
    channel.send("Server is down!")

#  Email   → Server is down!
#  SMS     → Server is down!
#  Push    → Server is down!
Enter fullscreen mode Exit fullscreen mode

Tomorrow you add SlackAlert(Notification) — the loop doesn't change. Not a single line of calling code is modified.

Key insight: This is the power of dynamic dispatch — Python doesn't look at the declared type. It looks at the actual object in memory and calls its version of send().


How All Four Work Together

The four pillars aren't isolated ideas — they reinforce each other in every well-designed system:

Encapsulation  →  protects the data inside each object
Abstraction    →  defines the contract each object must follow
Inheritance    →  lets new objects reuse and extend existing ones
Polymorphism   →  lets one piece of code work with all of them
Enter fullscreen mode Exit fullscreen mode

A real example — the notification system, fully composed:

from abc import ABC, abstractmethod

class Notification(ABC):               # Abstraction — defines contract
    def __init__(self, recipient):
        self.__recipient = recipient   # Encapsulation — private data

    def get_recipient(self):
        return self.__recipient

    @abstractmethod
    def send(self, msg): pass          # enforced contract


class Email(Notification):             # Inheritance — reuses __init__
    def send(self, msg):               # Polymorphism — own behaviour
        print(f"Email to {self.get_recipient()}: {msg}")

class SMS(Notification):
    def send(self, msg):
        print(f"SMS to {self.get_recipient()}: {msg}")


def notify_all(channels, msg):         # works for ANY Notification
    for ch in channels:
        ch.send(msg)

notify_all([Email("dev@co.com"), SMS("+91-9999999999")], "Deploy done!")
# Email to dev@co.com: Deploy done!
# SMS to +91-9999999999: Deploy done!
Enter fullscreen mode Exit fullscreen mode

All four pillars, twelve lines of clean code.


Quick Reference Cheat Sheet

Pillar Core Idea Python Mechanism Real-World Anchor
Encapsulation Hide data, expose behaviour __private, getters/setters ATM, Bank Account
Abstraction Hide implementation, show interface ABC, @abstractmethod Payment Gateway
Inheritance Reuse and extend class Child(Parent), super() Employee → Manager
Polymorphism One call, many behaviours Method overriding, dynamic dispatch Notification channels

TL;DR

  • Encapsulation = your data has a bodyguard
  • Abstraction = your caller doesn't need to know the internals
  • Inheritance = don't repeat yourself across related classes
  • Polymorphism = write once, works for every type

OOP isn't just a set of rules. It's a way of thinking — modeling the real world in code, with clear responsibilities, clean contracts, and zero unnecessary coupling.


Found this useful? Drop a ❤️ and follow for more backend and system design content.

Next up → SOLID Principles: the rules that make OOP actually work at scale.

Top comments (0)