Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "Objects", which contain both Data (in the form of fields or attributes) and Code (in the form of procedures or methods). Unlike procedural programming, where functions operate on separate detached global variables, OOP bundles data and the functions that manipulate that data into self-contained entities.
| Feature | Procedural Programming (POP) | Object-Oriented Programming (OOP) |
|---|---|---|
| Focus | Focuses on verbs / actions (step-by-step algorithms). | Focuses on nouns / entities (data objects and their behavior). |
| Data Security | Low: Data moves freely and can be modified by any function. | High: Data is encapsulated and hidden from unauthorized access. |
| Code Reuse | Limited to function calls. | Massive reuse through class inheritance and composition. |
| Suitability | Small mathematical scripts and CLI utilities. | Enterprise software, GUI apps, games, large cloud systems. |
β’ A Class is a user-defined prototype or blueprint that defines the attributes (variables) and methods (functions) that characterize any object of its type. It occupies no memory for data until instantiated.
β’ An Object (or instance) is a concrete realization of a class allocated in computer RAM. Each object has its own unique copy of instance variables.
name, roll_no, display()s1 = Student("Aman", 101)s2 = Student("Priya", 102)β’ The `__init__()` Method: Known as the constructor in Python. It is automatically called by CPython the instant a new object is instantiated from a class. Its primary role is to initialize the object's initial state (assigning instance variables).
β’ The `self` Parameter: Represents the current instance of the class. When you write student1.display(), Python automatically translates it to Student.display(student1) behind the scenes. `self` allows methods to know which specific object's data they should access or modify.
| Characteristic | Instance Variables (self.name) |
Class / Static Variables (college_name) |
|---|---|---|
| Where Defined? | Inside constructor or methods using self. |
Directly inside class body, outside any method. |
| Scope & Memory | Unique to each individual object in RAM. | Single copy shared equally across all objects. |
| Modifying Value | Modifying for obj1 affects ONLY obj1. |
Modifying via Class.var updates all objects! |
class Student:
# Class Variable: Shared by ALL student objects
college_name = "DevAscent Academy"
# Constructor: Initializes unique attributes per instance
def __init__(self, name, roll_no, gpa):
self.name = name # Instance variable
self.roll_no = roll_no # Instance variable
self.gpa = gpa # Instance variable
# Instance Method: Uses self to access calling object's data
def get_details(self):
return f"Roll #{self.roll_no}: {self.name} | GPA: {self.gpa} | College: {Student.college_name}"
# Creating two separate instances in memory
s1 = Student("Aman Gupta", 101, 8.9)
s2 = Student("Neha Sharma", 102, 9.4)
print(s1.get_details())
print(s2.get_details())
# Proof of Class Variable sharing: Update college name once
Student.college_name = "DevAscent University"
print("\nAfter updating Class Variable:")
print(f"{s1.name}'s College -> {s1.college_name}")
print(f"{s2.name}'s College -> {s2.college_name}")
Encapsulation is the practice of keeping fields within a class private, then providing access to them via public methods (Getters and Setters). It prevents accidental direct modification of critical internal state and ensures data validation rules are strictly respected before changes occur.
| Access Level | Python Syntax | Accessibility | How Python Enforces It |
|---|---|---|---|
| Public | self.balance |
Accessible from anywhere (inside & outside class). | Standard direct attribute access. |
| Protected | self._balance |
Accessible within class and subclasses. | By Convention: Single underscore signals "do not touch outside". |
| Private | self.__balance |
Accessible ONLY within declaring class. | Name Mangling: Renamed internally to _ClassName__balance. |
class BankAccount:
def __init__(self, account_holder, initial_balance):
self.holder = account_holder # Public
self.__balance = initial_balance # PRIVATE: double underscore prevents direct tampering!
# Getter: Securely read balance
def get_balance(self):
return self.__balance
# Setter: Enforce business validation rules
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive!")
self.__balance += amount
return self.__balance
account = BankAccount("Rahul Verma", 5000)
print("Account Holder:", account.holder)
print("Current Balance:", account.get_balance())
account.deposit(2500)
print("Balance after βΉ2500 deposit:", account.get_balance())
# Attempting direct access to private variable throws an AttributeError!
try:
print(account.__balance)
except AttributeError:
print("π BLOCKED: 'BankAccount' object has no attribute '__balance' (Private Protection!)")
Inheritance is a mechanism in which a new class (known as the child, derived, or subclass) is created from an existing class (known as the parent, base, or superclass). The child class automatically inherits all public and protected attributes and methods of the parent, enabling rapid code reuse and hierarchical categorization.
Employee β ManagerVehicle β Car β ElectricCar(Camera, Phone) β SmartphoneShape β Circle, Square, TriangleThe built-in super() function gives access to methods and constructor of the parent class. In child class constructors, calling super().__init__(...) passes arguments up to the parent so that foundational attributes are initialized cleanly without code duplication.
# Parent Base Class
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"Hello, my name is {self.name} and I am {self.age} years old."
# Child Derived Class (Single Inheritance)
class Professor(Person):
def __init__(self, name, age, department, salary):
# Delegate core attributes to Person's constructor via super()
super().__init__(name, age)
self.department = department
self.salary = salary
# Extended method specific to Professor
def conduct_lecture(self, topic):
return f"Prof. {self.name} ({self.department}) is delivering lecture on: {topic}"
prof = Professor("Dr. Arvind Mehta", 48, "Computer Science", 120000)
print(prof.introduce()) # Inherited directly from Person!
print(prof.conduct_lecture("Algorithms & Dynamic Programming"))
β’ Polymorphism (Greek for "many forms") is the ability to use the same interface or method call across different underlying types. For example, a single function render() can draw circles, squares, or polygons depending on the object passed to it.
β’ Method Overriding: Occurs when a child class provides its own specific implementation of a method that is already defined in its parent class. When called on the child instance, the child's version executes, overriding the parent's generic logic.
β’ Duck Typing: Python's dynamic typing philosophy: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check explicit inheritance typesβit simply checks if the required method exists!
class PaymentMethod:
def pay(self, amount):
raise NotImplementedError("Subclasses must implement pay()!")
# Child 1: Overrides pay() with Credit Card logic
class CreditCardPayment(PaymentMethod):
def __init__(self, card_no):
self.card_no = card_no[-4:]
def pay(self, amount):
return f"π³ Paid βΉ{amount} using Credit Card ending in **{self.card_no}"
# Child 2: Overrides pay() with UPI logic
class UPIPayment(PaymentMethod):
def __init__(self, upi_id):
self.upi_id = upi_id
def pay(self, amount):
return f"π± Paid βΉ{amount} via UPI handle: {self.upi_id}"
# Polymorphic Dispatcher Function: Works with ANY payment object!
def checkout(payment_gateway, bill_amount):
print(payment_gateway.pay(bill_amount))
payments = [
CreditCardPayment("4532118944528901"),
UPIPayment("shivan@okaxis")
]
for p in payments:
checkout(p, 2499)
Abstraction involves hiding background details and displaying only essential features to the user. In Python, abstraction is enforced using the abc module (Abstract Base Classes).
An Abstract Class serves as a strict structural contract: it defines methods that all child classes must implement, and cannot be instantiated directly!
from abc import ABC, abstractmethod
import math
# Abstract Contract Class
class Shape(ABC):
@abstractmethod
def calculate_area(self):
"""Every concrete child class MUST implement calculate_area."""
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
# Fulfilling the abstract contract
def calculate_area(self):
return round(math.pi * (self.radius ** 2), 2)
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
# Fulfilling the abstract contract
def calculate_area(self):
return self.length * self.width
c = Circle(7)
r = Rectangle(10, 5)
print("Circle Area :", c.calculate_area())
print("Rectangle Area:", r.calculate_area())
# Attempting to instantiate an Abstract Class fails immediately!
try:
s = Shape()
except TypeError as err:
print("π Abstraction Enforced:", err)
Dunder methods (short for "Double Underscore" methods) are predefined special methods in Python that have double underscores at the start and end of their names (e.g., __init__, __str__, __len__, __eq__). They allow your custom objects to integrate seamlessly with built-in Python operators and functions like print(), len(), and +.
| Method | Triggered By | Primary Intended Audience |
|---|---|---|
__str__(self) |
print(obj) or str(obj) |
End Users: Returns clean, readable, friendly informal description. |
__repr__(self) |
Typing object in Python interactive console | Developers: Unambiguous representation, ideally valid Python code to recreate the object. |
__len__(self) |
len(obj) |
Returns length/item count of custom container objects. |
__add__(self, other) |
obj1 + obj2 |
Customizes addition operator behavior (Operator Overloading). |
class Book:
def __init__(self, title, author, price):
self.title = title
self.author = author
self.price = price
# User-facing string representation for print()
def __str__(self):
return f"π '{self.title}' by {self.author} (βΉ{self.price})"
# Developer-facing unambiguous representation for debugging
def __repr__(self):
return f"Book(title='{self.title}', author='{self.author}', price={self.price})"
b = Book("Fluent Python", "Luciano Ramalho", 1850)
print("Calling print(b) -> Triggers __str__ :", b)
print("Calling repr(b) -> Triggers __repr__:", repr(b))
This capstone combines all 10 OOP concepts: an abstract base class (BankAccount), concrete subclasses with inheritance (SavingsAccount, CurrentAccount), private balance encapsulation, method overriding for interest calculation, polymorphism, and __str__() dunder formatting.
from abc import ABC, abstractmethod
# 1. Abstraction
class BankAccount(ABC):
def __init__(self, account_no, holder, initial_balance):
self.account_no = account_no
self.holder = holder
self.__balance = initial_balance # 2. Encapsulation: Private state
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return self.__balance
raise ValueError("Deposit must be positive!")
def _withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
@abstractmethod
def apply_monthly_interest(self):
pass
# 3. Special Dunder Method
def __str__(self):
return f"[{self.__class__.__name__}] #{self.account_no} ({self.holder}) Balance: βΉ{self.__balance:,.2f}"
# 4. Inheritance & Method Overriding
class SavingsAccount(BankAccount):
INTEREST_RATE = 0.04 # 4% annual interest
def apply_monthly_interest(self):
interest = self.get_balance() * (self.INTEREST_RATE / 12)
self.deposit(round(interest, 2))
return interest
class CurrentAccount(BankAccount):
OVERDRAFT_LIMIT = 25000
def apply_monthly_interest(self):
return 0.0 # Current accounts do not earn interest
# 5. Polymorphic Execution
accounts = [
SavingsAccount("SB-1001", "Ananya Roy", 50000),
CurrentAccount("CA-5002", "Apex Technologies Ltd", 180000)
]
print("--- Monthly Batch Processing ---")
for acc in accounts:
interest_earned = acc.apply_monthly_interest()
print(acc)
print(f" -> Monthly Interest Credited: βΉ{interest_earned:.2f}\n")
self represents the current instance of the class. It allows methods to access and modify the specific attributes and methods belonging to the object that invoked them. Python passes the instance reference to self automatically.
self.name; each object maintains its own distinct copy in memory.self.__balance). Python triggers Name Mangling, automatically renaming the variable internally to _ClassName__balance to prevent direct outside access.
*args are used instead).
from abc import ABC, abstractmethod) defines a mandatory blueprint/contract for subclasses. Any class with an @abstractmethod cannot be instantiated directly, forcing all child classes to implement that method.
__str__(): Returns a clean, user-friendly string representation intended for end-users when calling print(obj).__repr__(): Returns an unambiguous, detailed representation intended for developers and debugging, showing the exact code needed to recreate the object.