Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Blog
Next

Python OOP: Beginner Friendly Guide

A complete guide to the 4 pillars of OOP, real-world analogies, clean code snippets, and why this changes how you build software.

🕐 15 min read🐍 Python 3.x🎯 Beginners

Introduction

What exactly is OOP?

Imagine you're building a car manufacturing simulation. You need to keep track of hundreds of cars — each with a brand, model, color, speed, and fuel level. You also need functions to accelerate, brake, refuel, and honk.

In traditional programming, all of this would be a mess of variables and functions floating around with no real structure. Object-Oriented Programming (OOP) solves this by letting you bundle data and behavior together into a single, clean unit called an object.

💡 Core Idea: OOP is a way of thinking about code in terms of real-world things. You model your program as a collection of objects that talk to each other — just like the real world works.

Python is one of the most beginner-friendly languages to learn OOP in. Its syntax is clean, readable, and close to plain English. By the end of this guide, you'll understand all 4 pillars of OOP deeply — with real examples and Python code that you can run today.


The 4 Pillars at a Glance

  • 🔒 Encapsulation: Bundling data and methods together, and hiding internal details from the outside world.
  • 🧬 Inheritance: Creating new classes that inherit properties and methods from existing ones, enabling code reuse.
  • 🎭 Polymorphism: Allowing objects of different types to be treated through the same interface in different ways.
  • 🎨 Abstraction: Hiding complex implementation details and exposing only what is necessary to the user.

01 — Why OOP?

Procedural Programming: The Before Picture

Before diving into OOP, let's appreciate what came before it — and why OOP was invented. Procedural programming organizes code as a sequence of instructions (procedures / functions) that run top to bottom.

Advantages of Procedural Programming:

  • ⚡ Simple & Fast: Easy to learn, easy to read top-to-bottom for small scripts.
  • 🎯 Straightforward Logic: Great for scripts, automation tasks, and small programs.
  • 🔁 Code Reuse via Functions: Keeps code DRY at a basic level.
  • 🧪 Easy to Debug Small Code: Tracing the flow line-by-line is natural.

Why Procedural Falls Short: As programs grow, procedural code gets messy. Here's the same "bank account" problem written both ways:

⚠️ Procedural Way (Chaos at Scale)

# Data is just loose variables
account_name = "Arjun"
account_balance = 1000
 
def deposit(balance, amount):
    return balance + amount
 
def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount
 
# With 100 accounts this becomes chaos!
account_balance = deposit(account_balance, 500)

✅ OOP Way (Clean & organized)

# Data + behavior in one clean unit
class BankAccount:
    def __init__(self, name, balance):
        self.name    = name
        self.balance = balance
 
    def deposit(self, amount):
        self.balance += amount
 
    def withdraw(self, amount):
        self.balance -= amount
 
# 100 accounts? No problem at all.
acc = BankAccount("Arjun", 1000)
acc.deposit(500)

📌 Rule of Thumb: Use procedural for small scripts and one-off tasks. Use OOP when your program models real-world entities, grows over time, or is worked on by a team.


02 — Foundation: Classes & Objects

Before the 4 pillars, you need to understand two terms deeply: Class and Object.

ConceptWhat it isReal-Life Analogy
ClassA blueprint or template that defines structure and behaviorThe architectural blueprint of a house
ObjectA specific instance created from a classAn actual house built from that blueprint
AttributeA variable that belongs to an objectThe color, size, rooms of a specific house
MethodA function that belongs to an objectActions like "open door", "turn on lights"

Defining a Class and Creating Objects

# CLASS = Blueprint
class Dog:
    # __init__ is the constructor — runs when an object is created
    def __init__(self, name, breed, age):
        self.name  = name    # attribute
        self.breed = breed   # attribute
        self.age   = age     # attribute
 
    def bark(self):            # method
        print(f"{self.name} says: Woof!")
 
    def describe(self):         # method
        print(f"{self.name} is a {self.age}-year-old {self.breed}.")
 
# OBJECTS = Instances built from the blueprint
dog1 = Dog("Bruno", "Labrador", 3)
dog2 = Dog("Bella", "Poodle", 5)
 
dog1.bark()       # Bruno says: Woof!
dog2.describe()   # Bella is a 5-year-old Poodle.
 
# Each object has its OWN data
print(dog1.name)  # Bruno
print(dog2.name)  # Bella

Notice how self always appears as the first parameter — it's Python's way of saying "this specific object."


03 — Pillar 1: Encapsulation 🔒

Encapsulation means keeping an object's internal state private and only allowing the outside world to interact with it through controlled, well-defined methods. Think of it as a capsule that protects what's inside.

💊 Real-Life Analogy: An ATM machine. You (the outside) press buttons and insert your card, but you never directly touch the internal cash mechanism or the computer inside. The ATM exposes only what you need — a PIN pad and a display.

In Python, you make an attribute private by prefixing it with double underscores (__). Private attributes can't be accessed directly — they must go through methods called getters and setters.

Encapsulation — Bank Account

class BankAccount:
    def __init__(self, owner, initial_balance=0):
        self.owner       = owner
        self.__balance   = initial_balance  # __ makes it PRIVATE
        self.__transactions = []
 
    # GETTER — controlled read access
    def get_balance(self):
        return self.__balance
 
    # SETTER with validation
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive!")
        self.__balance += amount
        self.__transactions.append(f"+{amount}")
 
    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient funds!")
        self.__balance -= amount
        self.__transactions.append(f"-{amount}")
 
    def get_statement(self):
        return self.__transactions
 
# Usage
acc = BankAccount("Arjun", 5000)
acc.deposit(2000)
acc.withdraw(500)
print(acc.get_balance())     # 6500
print(acc.get_statement())   # ['+2000', '-500']
 
# This would FAIL — good! Data is protected.
# print(acc.__balance)  → AttributeError

Why does this matter? Without encapsulation, any part of your code could accidentally set balance = -99999. With encapsulation, every change goes through a method that validates the data first.


04 — Pillar 2: Inheritance 🧬

Inheritance allows a new class (subclass) to acquire the properties and methods of an existing class (superclass). This is one of the most powerful features for code reuse.

🧬 Real-Life Analogy: Think of vehicles. Every vehicle has wheels, an engine, and can move. But a Car, a Truck, and a Motorcycle each add their own specific features. Instead of rewriting "has engine" for each — they all inherit from a parent Vehicle class.

Inheritance — Vehicle System

# PARENT CLASS
class Vehicle:
    def __init__(self, brand, model, fuel_type):
        self.brand     = brand
        self.model     = model
        self.fuel_type = fuel_type
        self.speed     = 0
 
    def accelerate(self, amount):
        self.speed += amount
        print(f"{self.brand} accelerates to {self.speed} km/h")
 
    def brake(self):
        self.speed = 0
        print(f"{self.brand} stops.")
 
    def info(self):
        print(f"{self.brand} {self.model} ({self.fuel_type})")
 
# CHILD CLASS — inherits Vehicle, adds more
class Car(Vehicle):
    def __init__(self, brand, model, fuel_type, doors):
        super().__init__(brand, model, fuel_type)  # call parent
        self.doors = doors                          # new attribute
 
    def open_trunk(self):
        print(f"Trunk of {self.brand} opened!")
 
# ANOTHER CHILD CLASS
class ElectricCar(Car):           # Multi-level inheritance!
    def __init__(self, brand, model, battery_kwh):
        super().__init__(brand, model, "Electric", 4)
        self.battery_kwh = battery_kwh
 
    def charge(self):
        print(f"Charging {self.brand} ({self.battery_kwh} kWh)...")
 
# Usage
my_car = Car("Honda", "City", "Petrol", 4)
my_car.accelerate(60)     # Inherited from Vehicle!
my_car.open_trunk()       # Car-specific method
 
tesla = ElectricCar("Tesla", "Model 3", 75)
tesla.accelerate(100)     # Works! Inherited from Vehicle
tesla.charge()            # ElectricCar-specific
tesla.open_trunk()        # Inherited from Car

🔑 Key Concept: super() lets you call the parent class's __init__ to initialize the inherited attributes before adding your own.


05 — Pillar 3: Polymorphism 🎭

Polymorphism means "many forms." In Python, it allows different objects to respond to the same method call in their own way.

🔌 Real-Life Analogy: Think of a universal remote control. You press "Volume Up" — but it works differently on a TV, a soundbar, and an air conditioner. The same button, different behavior.

Polymorphism — Payment Gateway

class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("Subclass must implement pay()")
 
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
 
    def pay(self, amount):  # Override parent's pay()
        print(f"Charged ₹{amount} to card ending in {self.card_number[-4:]}")
 
class UPI(PaymentMethod):
    def __init__(self, upi_id):
        self.upi_id = upi_id
 
    def pay(self, amount):
        print(f"₹{amount} sent via UPI to {self.upi_id}")
 
class Wallet(PaymentMethod):
    def __init__(self, wallet_name, balance):
        self.wallet_name = wallet_name
        self.balance     = balance
 
    def pay(self, amount):
        self.balance -= amount
        print(f"₹{amount} paid from {self.wallet_name}. Balance: ₹{self.balance}")
 
# POLYMORPHISM IN ACTION
# All different objects, but we treat them the SAME way
payment_methods = [
    CreditCard("4111111111112024"),
    UPI("arjun@upi"),
    Wallet("Paytm", 2000)
]
 
for method in payment_methods:
    method.pay(500)   # Same call, different behavior!

06 — Pillar 4: Abstraction 🎨

Abstraction means showing only the essential features of something and hiding the complex implementation. Users of your class don't need to know how it works internally — they just need to know what it does.

📱 Real-Life Analogy: When you tap "Send" on your phone, you don't think about TCP/IP packets, network routing, server responses, or encryption. You just tap "Send." All that complexity is abstracted away.

Abstraction — Notification System

from abc import ABC, abstractmethod
 
# ABSTRACT CLASS — defines the contract
class Notification(ABC):
    @abstractmethod
    def send(self, recipient, message):
        """Every notification type MUST implement this."""
        pass
 
    @abstractmethod
    def get_channel_name(self):
        pass
 
    # Non-abstract shared method
    def notify(self, recipient, message):
        print(f"[{self.get_channel_name()}] → {recipient}")
        self.send(recipient, message)
 
# CONCRETE CLASS — fills in the contract
class EmailNotification(Notification):
    def send(self, recipient, message):
        print(f"Email sent to {recipient}: {message}")
 
    def get_channel_name(self):
        return "EMAIL"
 
class SMSNotification(Notification):
    def send(self, recipient, message):
        print(f"SMS sent to {recipient}: {message}")
 
    def get_channel_name(self):
        return "SMS"
 
# Usage
channels = [EmailNotification(), SMSNotification()]
 
for channel in channels:
    channel.notify("user@example.com", "Your order is confirmed!")

⚠️ Abstraction vs Encapsulation: Encapsulation hides data. Abstraction hides complexity.


07 — Putting it All Together

Let's build a mini e-commerce order system that uses all 4 pillars together.

from abc import ABC, abstractmethod
 
# ── ABSTRACTION: Define what every product must have
class Product(ABC):
    def __init__(self, name, price):
        self.name       = name
        self.__price    = price    # ENCAPSULATION
 
    def get_price(self):
        return self.__price
 
    @abstractmethod
    def get_description(self):
        pass
 
    @abstractmethod
    def calculate_tax(self):
        pass
 
# ── INHERITANCE: Specific product types inherit Product
class Electronics(Product):
    def __init__(self, name, price, warranty_years):
        super().__init__(name, price)
        self.warranty_years = warranty_years
 
    def get_description(self):        # POLYMORPHISM
        return f"{self.name} ({self.warranty_years}yr warranty)"
 
    def calculate_tax(self):          # POLYMORPHISM
        return self.get_price() * 0.18
 
class Clothing(Product):
    def __init__(self, name, price, size):
        super().__init__(name, price)
        self.size = size
 
    def get_description(self):
        return f"{self.name} (Size: {self.size})"
 
    def calculate_tax(self):
        return self.get_price() * 0.05
 
# ── ENCAPSULATION: Order hides its internal cart logic
class Order:
    def __init__(self, customer_name):
        self.customer      = customer_name
        self.__cart        = []
        self.__is_confirmed = False
 
    def add_item(self, product):
        if self.__is_confirmed:
            print("Cannot modify a confirmed order!")
            return
        self.__cart.append(product)
 
    def get_total(self):
        # POLYMORPHISM: calculate_tax works differently per product!
        subtotal = sum(p.get_price() + p.calculate_tax() for p in self.__cart)
        return round(subtotal, 2)
 
    def confirm(self):
        self.__is_confirmed = True
        print(f"Order confirmed for {self.customer}! Total: ₹{self.get_total()}")
        for p in self.__cart:
            print(f"  • {p.get_description()} — ₹{p.get_price()}")
 
# ── IN ACTION
phone  = Electronics("iPhone 15", 79999, 1)
tshirt = Clothing("Polo T-Shirt", 999, "L")
 
order = Order("Arjun")
order.add_item(phone)
order.add_item(tshirt)
order.confirm()

08 — Summary Cheat Sheet

Bookmark this section. You'll refer back to it often.

PillarCore IdeaReal-World Benefit
🔒 EncapsulationBundle data + behavior; hide internalsPrevents bugs, enables validation
🧬 InheritanceChild classes extend parent classesWrite once, extend many times
🎭 PolymorphismSame interface, different behaviorExtensible systems
🎨 AbstractionHide complex implementationSimpler APIs, cleaner design

Next Steps:

  1. Build a mini project using all 4 pillars — e.g., a Library Management System.
  2. Explore Python's dataclasses for cleaner definitions.
  3. Study how Django or Flask use OOP internally.

⭐ Did you find this guide helpful?

If this article helped you understand Object-Oriented Programming, I'd really appreciate it if you gave my portfolio repository a star! It helps me create more content like this.