
Object-Oriented Programming (OOP) is a programming style where software is designed around objects instead of long sequences of functions. Each object represents a real-world entity and contains both data and behavior. This approach makes programs more realistic, structured, and easier to manage, especially when dealing with large or complex applications.
OOP is used because it makes complex code easier to organize and maintain by dividing a program into independent, reusable components. When applications grow bigger, procedural code becomes harder to extend or debug. OOP solves this by grouping related data and functionality together, allowing each object to manage its own behavior. This reduces errors, increases reusability, and helps developers scale applications safely over time.
OOP is built on four major principles—Encapsulation, Abstraction, Inheritance, and Polymorphism. These concepts allow developers to hide internal details, reuse existing code, structure programs in logical layers, and write flexible functions that can work with different types of objects. Together, these features create robust, maintainable, and extendable software systems used in modern development.
A class is a blueprint or template used to create objects. It defines the structure of something by describing what data it stores and what actions it can perform. The class itself does not hold actual data; instead, it defines how objects created from it will behave. You can think of a class like an architectural plan of a building — it lays out the design, but the actual building is constructed only when an object is created.
An object is an instance created from a class. It contains real data and can perform actions defined in the class. Each object has its own copy of the attributes, allowing multiple objects from the same class to have different values. If a class is the template, an object is the actual product built from that template, like a real house built using the architectural plan.
class Car:
def start(self):
print("Car has started.")my_car = Car() # Creating an object
my_car.start() # Calling method through object
Output:
Car has started.
This example shows a simple class with one method. When my_car is created, it becomes an actual object that can call the method defined inside the class.

The constructor is a special method in Python that runs automatically every time an object of a class is created. Its main purpose is to initialize the object with starting values so that each object begins in a valid, ready-to-use state. Without a constructor, you would need to manually assign values to every object after creating it, which becomes inefficient and error-prone. The constructor ensures that all objects created from a class have the required data set up immediately.
self refers to the current object (instance) that is being created or used. It works like a pointer to the object itself, allowing each object to store its own data independently. When you write self.name = name, you are storing the value inside the object so that it can be used later by other methods. Without self, Python would not know which object’s data you are referring to, making object-specific operations impossible.
class Student:
def __init__(self, name, age):
self.name = name # stored inside object
self.age = agedef show(self):
print("Name:", self.name, "| Age:", self.age)s1 = Student("Aarav", 20)
s2 = Student("Meera", 19)s1.show()
s2.show()
Output:
Name: Aarav | Age: 20
Name: Meera | Age: 19
This example shows how each object maintains separate data using self, even though both are created from the same class.
Attributes represent the data stored inside an object or a class. They define the state of the object. In Python, attributes come in two types: instance attributes and class attributes. Both are important, and understanding their differences is essential for writing clean and structured OOP code.
Instance attributes belong to individual objects. Each object gets its own copy of these attributes, meaning the values stored inside one object do not affect another. Instance attributes are almost always defined inside the constructor using self, because self refers to the specific object being created.
Instance attributes are used when every object should store its own separate data. For example, every student has a different name, age, and roll number, so these become instance attributes.
class Employee:
def __init__(self, name, salary):
self.name = name # instance attribute
self.salary = salary # instance attributee1 = Employee("Rohan", 50000)
e2 = Employee("Anita", 60000)print(e1.name, e1.salary)
print(e2.name, e2.salary)
Output:
Rohan 50000
Anita 60000
Each employee object maintains its own data.
Class attributes belong to the class itself and are shared among all objects created from that class. This means if you change a class attribute, the change reflects across every object—unless an object overrides it with its own instance attribute.
Class attributes are used for values that should be the same for all objects. Good examples include a common tax rate, a company name, or the total number of created objects.
These attributes are defined directly inside the class but outside any method.
class Car:
wheels = 4 # class attribute (shared by all objects)def __init__(self, model):
self.model = model # instance attributec1 = Car("Honda City")
c2 = Car("Toyota Fortuner")print(c1.model, "-", c1.wheels)
print(c2.model, "-", c2.wheels)
Output:
Honda City - 4
Toyota Fortuner - 4
Here, both cars share the same number of wheels because it is a class-level attribute.
Methods define the behavior of objects. Just like attributes represent data, methods represent actions. They are functions written inside a class and can operate on instance data, class-level data, or act as utility functions.
Python provides three main types of methods, each serving a different purpose:
Understanding when and why to use each type helps in designing cleaner and more maintainable class structures.
Instance methods are the most common type of methods in Python. They operate on individual objects and can access both instance attributes and other instance methods. They always include self as their first parameter, which gives them access to the object’s own data.
Instance methods are used when the behavior depends on the specific object’s state. For example, displaying a student’s details or updating a product price.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marksdef display(self): # instance method
print(f"{self.name} scored {self.marks} marks.")s1 = Student("Aman", 88)
s1.display()
Output:
Aman scored 88 marks.
Class methods work at the class level instead of the object level. They receive the class itself as the first argument, which is written as cls by convention. Class methods cannot directly access instance attributes, but they can modify class attributes or create alternative constructors.
Use class methods when you want behavior related to the entire class, not a specific object. A common use-case is keeping count of total objects or creating objects in different ways.
class Employee:
count = 0 # class attributedef __init__(self, name):
self.name = name
Employee.count += 1@classmethod
def total_employees(cls):
return f"Total employees = {cls.count}"e1 = Employee("Ravi")
e2 = Employee("Neha")print(Employee.total_employees())
Output:
Total employees = 2
Static methods are independent utility functions placed inside a class for organizational purposes. They do not receive self or cls, meaning they cannot access object data or class data directly.
Static methods are useful when behavior logically belongs to the class but does not need access to object or class attributes—for example, helper calculations, validations, or format conversions.
class MathTools:
@staticmethod
def add(a, b):
return a + b
print(MathTools.add(5, 7))
Output:
12
Static methods help keep related functionality inside the class without depending on class or instance data.
Encapsulation is one of the core principles of OOP and focuses on protecting data inside a class. It ensures that important internal details are hidden from direct external access, allowing better control and security over how the data is used or modified. In Python, encapsulation is implemented using naming conventions because Python does not enforce strict access modifiers like some other languages.
Encapsulation helps prevent accidental modification of sensitive data and makes the class easier to maintain over time. Instead of exposing internal variables directly, we provide controlled access through methods (getters/setters) or properties.
Python uses simple naming rules to indicate how accessible a variable or method is supposed to be. These rules are conventions and not absolute restrictions, but they guide good programming practice.
Public attributes and methods are accessible from anywhere.
They are defined without any underscore prefix.
class Car:
def __init__(self):
self.brand = "BMW" # publicc = Car()
print(c.brand)
Output:
BMW
Protected attributes use a single underscore prefix.
This indicates that the member is intended for internal use within the class and its subclasses, but Python still allows access without error.
class Car:
def __init__(self):
self._speed = 120 # protected attributec = Car()
print(c._speed) # still accessible, but should be avoided
Output:
120
Protected members act as a "warning" to programmers rather than a strict barrier.
Private attributes use a double underscore prefix.
Python performs name mangling, which renames the variable internally to make accidental access harder.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # private attributedef get_balance(self):
return self.__balanceacc = BankAccount(5000)
print(acc.get_balance())
Output:
5000
Attempting direct access fails:
print(acc.__balance) # Error
Why?
The variable is internally renamed to _BankAccount__balance.
Encapsulation ensures safety and structure in your programs. It allows you to expose only what is necessary while keeping the internal implementation flexible and secure. If later you want to change how data is stored or validated, you only need to modify the internal code, not the external code using the class.
Here’s a real-world example: A bank doesn’t allow anyone to directly modify account balances. Instead, it provides deposit and withdrawal methods that control and validate every change. Similarly, encapsulation ensures that the internal state of a class remains consistent and safe.
Abstraction is an OOP concept that focuses on showing only the essential features of an object while hiding the unnecessary implementation details. It allows you to design clear and simple interfaces while keeping complex internal logic hidden from the user. This makes programs easier to understand, extend, and maintain.
In real life, abstraction is everywhere. When you drive a car, you only interact with the steering wheel, accelerator, and brakes. You do not need to understand how the engine burns fuel or how the gearbox shifts. Python provides tools that allow us to design such clean and simplified interfaces in code as well.
In Python, abstraction is commonly implemented using Abstract Base Classes (ABC). These classes act as templates that define what methods must exist, but they leave the implementation details to the subclasses. An abstract class cannot be instantiated directly; it only guides how child classes should behave.
Python provides the abc module to create abstract classes and methods.
An abstract class typically contains one or more abstract methods, defined using the @abstractmethod decorator. These methods must be implemented in the child classes, ensuring a consistent interface.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
passclass Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.sidesq = Square(5)
print(sq.area())
Output:
25
The abstract class Shape defines the idea of an area calculation, but does not specify how it should be done. Each subclass provides its own implementation based on its unique characteristics.
An abstract method is a method that is declared but contains no implementation. It enforces a rule that subclasses must implement this method. This ensures that all subclasses follow the same structure or interface.
Abstract methods are especially useful in larger applications where different developers work on different parts of the system. It provides a contract that says: “Every subclass must follow this behavior.”