Python Object-Oriented Programming (OOP)

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data, or objects, rather than functions and logic. Python is an inherently object-oriented language, meaning it fully supports OOP concepts.

The core idea behind OOP is to model real-world entities as objects in your code. These objects combine both data (attributes) and behavior (methods).



Why OOP?



Four Pillars of OOP

1. Encapsulation : (Data hiding)

2. Abstraction : (Hiding Complexity)

3. Inheritance : (Reusing Code)

4. Polymorphism : (Many Forms)



Object :


Example :

obj = Car( )


class :


Example: Class and Object

class Employee:
    def __init__(self, fname, lname, salary):
        self.fname = fname          # First name attribute
        self.lname = lname          # Last name attribute
        self.salary = salary        # Salary attribute

# Creating objects of the Employee class
James = Employee('James', 'Smith', 50000)
John = Employee('John', 'Johnson', 40000)

# Accessing and printing the salary attributes
print(John.salary, James.salary)   # Output: 40000 50000

Other Method :

class Employee:
    def __init__(self,fname,lname,salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
    def info(self):
        print(self.fname)
        print(self.lname)
        print(self.salary)
emp = Employee("Robert","Williams",1000)
emp.info()

# Output:
  Robert
  Williams
  1000


What is __init__ :

__init__ is a constructor method, and automatically called to allocate memory, when a new object is created.



What is Self( ):

self( ) is used to represent the instance (object) of the class.

self( ) gives access to instance attribute - (self.age).

self( ) is automatically passed by python.



Types of variable in Python OOP :

A. Instance Variables: (Object level variable)

B. Static Variables: (Class level variable)

C. Local Variables: (Method level variable)



A. Instance Variables :

For every object a seperate copy will be created.


Where can we declare instance variables :



1. Inside Constructor (__init__( )) using self.

class Student:
    def __init__(self, name, age):
        self.name = name      # Instance variable
        self.age = age        # Instance variable


2. Inside Instance Method using self.

class Student:
    def set_grade(self, grade):
        self.grade = grade    # Instance variable created in method


3. Outside the Class using object reference.

s1 = Student("Vikas", 23)
s1.city = "Delhi"   # city is an instance variable added from outside


B. Static Variables :

Static variables (also called class variables) are variables that are shared among all objects of the class.

Only one copy of the variable exists at the class level, not per object.


How to Declare Static (Class) Variables in Python :

Static variable = Variable that belongs to the class, not to individual objects.



1. Directly inside the class (outside all methods).

class Student:
    school_name = "ABC School"  # Static variable


2. Inside the constructor using class name.

class Student:
    school_name = "ABC School"  # Static variable

Note: Use class name, not self, because static variable is not instance-specific.



3. Inside instance method using class name.

class Student:
    def set_school(self):
        Student.school_name = "XYZ School"

Avoid using self.school_name — it will create an instance variable instead.



4. Inside class method using cls or class name.

class Student:
    @classmethod
    def set_school(cls):
        cls.school_name = "LMN School"

cls is automatically passed to class methods — recommended way for modifying static variables.



5. Inside static method using class name.

class Student:
    @staticmethod
    def set_school():
        Student.school_name = "PQR School"

Static method does not take self or cls, so use class name.



6. From outside the class using class name.

Student.school_name = "DEF School"

This also directly modifies the class variable — and affects all instances.



C. Local Variables :

Local variables are variables that are declared inside a method/function/block and can be accessed only within that method.

These variables are created when the function is called and destroyed when the function ends.



Key Characteristics of Local Variables :


Feature Description
Scope Limited to the method/block where defined
Lifetime Exists only while the method is running
Accessed by Directly inside the function (no self, no cls)
Shared between objects? ❌ No (Each call has its own local variables)
Memory location Inside method’s stack frame
Not an attribute Not part of the object (__dict__) or class


Types of Methods :

In Python's object-oriented programming, there are 3 main types of methods defined inside a class:



1. Instance Method :

class Student:
    def __init__(self, name):
        self.name = name

    def greet(self):  # Instance method
        print(f"Hello, {self.name}")


2. Class Method :

class Student:
    school = "DPS"  # Class variable

    @classmethod
    def get_school(cls):  # Class method
        print(f"School Name: {cls.school}")


3. Static Method :

class MathUtils:
    @staticmethod
    def add(a, b):  # Static method
        return a + b


Note :



No decorator:- only two options.

  1. Instance method
  2. Static method


Setter and Getter Methods :

In Object-Oriented Programming, getter and setter methods are used to access and modify private data members of a class safely.



Why use Getter and Setter?



1. Getter Method :

Used to read/access the value of a private variable.

class Student:
    def __init__(self, name):
        self.__name = name   # private variable using __

    def get_name(self):      # Getter method
        return self.__name


2. Setter Method :

Used to modify/update the value of a private variable.

def set_name(self, new_name):   # Setter method
        if isinstance(new_name, str) and new_name != "":
            self.__name = new_name
        else:
            print("Invalid name")


Full Example :

class Student:
    def __init__(self, name):
        self.__name = name  # private variable

    def get_name(self):     # Getter
        return self.__name

    def set_name(self, new_name):  # Setter
        if isinstance(new_name, str) and new_name != "":
            self.__name = new_name
        else:
            print("Invalid name")

# Usage
s = Student("Vikas")
print(s.get_name())       # Output: Vikas

s.set_name("Rohan")       # Update name
print(s.get_name())       # Output: Rohan

s.set_name(123)           # Invalid name


Summary :


Method Type Purpose Syntax
Getter Access private data get_variable()
Setter Modify private data set_variable(value)


Employee Salary Increment?

class Employee:
    increment = 2.5
    def __init__(self,fname,lname,salary):
     self.fname = fname
     self.lname = lname
     self.salary = salary
    def increase(self):
        self.salary = self.salary * Employee.increment
Michael = Employee("Michael","Brown",5000)
Michael.increase()
print(Michael.salary)

# Output: 12500.0


celsius to fahrenheit?

def ctof(c):
    f = (c*9/5)+32
    print(f)
ctof(10)
ctof(20)
ctof(30)

# Output:
  50.0
  68.0
  86.0


Other Method : celsius to fahrenheit?

class Ctof:
    def __init__(self,c):
        self.c = c
    def ctof(self):
        f = (self.c*9/5) + 32
        print(f)
c1 = Ctof(10)   
c2 = Ctof(20)      #object
c3 = Ctof(30)
c4 = Ctof(40)
c1.ctof()
c2.ctof()
c3.ctof()
c4.ctof()

# Output:
  50.0
  68.0
  86.0
  104.0


Employee, name, employeeid, salary :

class Employee:
    def __init__(self,name,employeeid,salary):
        self.name = name
        self.employeeid = employeeid                 # These three are instance variables.
        self.salary = salary
    def talk(self):       #its not a function ,talk is a method
        print("hello my name is:",self.name)
        print("hello my employeeid is:",self.employeeid)
        print("hello my salary is:",self.salary)
emp1 = Employee("William",1234,56789)
emp2 = Employee("Jones",1290,56700)
emp1.talk()
emp2.talk()

# Output:
  hello my name is: William
  hello my employeeid is: 1234
  hello my salary is: 56789
  hello my name is: Jones
  hello my employeeid is: 1290
  hello my salary is: 56700


Movie Example :

class Movie:
    def __init__(self,name,hero,heroine,rating):
        self.name = name
        self.hero = hero
        self.heroine = heroine
        self.rating = rating
    def info(self):
        print("movie name:",self.name)
        print("movie hero:",self.hero)
        print("movie heroine:",self.heroine)
        print("movie rating:",self.rating)
mov = Movie("PK","AK","AS",8)
mov.info()

# Output:
  movie name: PK
  movie hero: AK
  movie heroine: AS
  movie rating: 8


Movie Example through for loop :

class Movie:
    def __init__(self,name,hero,heroine,rating):
        self.name = name
        self.hero = hero
        self.heroine = heroine
        self.rating = rating
    def info(self):   #info is method
        print("movie name:",self.name)
        print("movie hero:",self.hero)
        print("movie heroine:",self.heroine)
        print("movie rating:",self.rating)
movies = [Movie("PK","AK","AS",10),Movie("AB","CD","EF",10),Movie("GH","IJ","KL" ,6),Movie("MN","OP","QR",0 )]
for mov in movies:
    mov.info()

    
# Output:
  movie name: PK
  movie hero: AK
  movie heroine: AS
  movie rating: 10
  movie name: AB
  movie hero: CD
  movie heroine: EF
  movie rating: 10
  movie name: GH
  movie hero: IJ
  movie heroine: KL
  movie rating: 6
  movie name: MN
  movie hero: OP
  movie heroine: QR
  movie rating: 0

How to access the instance variable - Within the class by using self.

class Test:
    def __init__(self):
        self.a = 20         # Instance variable
        self.b = 30         # Instance variable

    def m1(self):           # Instance method
        print(self.a)       # Accessing a using self
        print(self.b)       # Accessing b using self

t = Test()                  # Creating object
t.m1()                      # Calling instance method

# Output - 
Value of a: 20  
Value of b: 30


How to Delete Instance Variables in Python.

In Python, we can delete instance variables using the del keyword with self inside the class.

class Test:
    def __init__(self):
        self.a = 10
        self.b = 20
        self.c = 30
        self.d = 40

    def m1(self):
        del self.b   # Deleting instance variable 'b'

# Create an instance of the Test class
t = Test()

# Print instance variables before deletion
print("Before deletion:", t.__dict__)

# Call method to delete variable 'b'
t.m1()

# Print instance variables after deletion
print("After deletion:", t.__dict__)

# Output - 
Before deletion: {'a': 10, 'b': 20, 'c': 30, 'd': 40}
After deletion: {'a': 10, 'c': 30, 'd': 40}


How to Modify and Add Instance Variables Dynamically in Python.

class Test:
    def __init__(self):
        self.a = 10    # Instance variable initialized

    def m1(self):
        self.a = 777   # Modifying existing variable
        self.b = 888   # New instance variable added

t = Test()

# Print instance variables before method call
print(t.__dict__)    # {'a': 10}

# Modify instance variables using method
t.m1()
print(t.__dict__)    # {'a': 777, 'b': 888}

# Modify 'b' and add new 'c' from outside the class
t.b = 1000
print(t.__dict__)    # {'a': 777, 'b': 1000}

t.c = 2000
print(t.__dict__)    # {'a': 777, 'b': 1000, 'c': 2000}


Print Fibonacci Series up to n.

def fib(n):
    """Print a Fibonacci series up to n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# Call the function with desired limit
fib(2000)

# Output : 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597


Return Fibonacci Series up to n (as a List).

def fib2(n):  
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a + b
    return result

# Call the function and store result
f100 = fib2(100)
print(f100)

# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]


Understanding Static Variables with __dict__ Example

class Test:
    a = 100  # Static variable

    def __init__(self):
        self.b = 200  # Instance variable

    def m1(self):
        self.c = 300  # Instance variable (added to object)

    @classmethod
    def m2(cls):
        cls.d = 400      # Class variable (added to class)
        Test.e = 500     # Also class variable

    @staticmethod
    def m3():
        Test.f = 600     # Static method adding class variable

# Create object
t = Test()

# 1. Before calling any method
print(Test.__dict__)  # Only a, not b/c/d/e/f

# 2. Call instance method (modifies object only)
t.m1()
print(Test.__dict__)  # No change in class dict

# 3. Call class method (adds d and e)
t.m2()
print(Test.__dict__)  # Now includes d and e

# 4. Call class method again (values overwrite)
t.m2()
print(Test.__dict__)  # d and e values still there

# Output : 
{'__module__': '__main__', 'a': 100, '__init__': , 'm1': , 'm2': , 'm3': , '__dict__': , '__weakref__': , '__doc__': None}
{'__module__': '__main__', 'a': 100, '__init__': , 'm1': , 'm2': , 'm3': , '__dict__': , '__weakref__': , '__doc__': None}
{'__module__': '__main__', 'a': 100, '__init__': , 'm1': , 'm2': , 'm3': , '__dict__': , '__weakref__': , '__doc__': None, 'd': 400, 'e': 500}
{'__module__': '__main__', 'a': 100, '__init__': , 'm1': , 'm2': , 'm3': , '__dict__': , '__weakref__': , '__doc__': None, 'd': 400, 'e': 500}


Difference Between Instance Variable and Static Variable in Python.

class Test:
    a = 777               # Static variable (class-level)

    def __init__(self):
        self.a = 888      # Instance variable (object-level)

# Create object
t = Test()

# Access instance variable
print(t.a)        # Output: 888

# Access static variable
print(Test.a)     # Output: 777

# Output : 
888
777


How to Access Static Variables in Python (OOP).

class Test:
    a = 10   # Static variable

    def __init__(self):
        print(self.a)     # 1 Access via self (inside constructor)
        print(Test.a)     # 1 Access via class name

    def m1(self):
        print(self.a)     # 2 Access via self (inside instance method)
        print(Test.a)     # 2 Access via class name

    @classmethod
    def m2(cls):
        print(cls.a)      # 3 Access via cls (inside class method)
        print(Test.a)     # 3 Access via class name

    @staticmethod
    def m3():
        print(Test.a)     # 4 Access via class name (inside static method)

# 5 Access from outside the class
t = Test()       # constructor runs
t.m1()
t.m2()
t.m3()
print(t.a)       # 5 via object reference
print(Test.a)    # 5 via class name

# Output : 
10
10
10
10
10
10
10
10
10


Important Note :

Even when accessed using self, cls, or object reference, the value still comes from the class-level static variable (unless overridden in the instance).



How to Update Static Variable Inside Constructor in Python.

class Test:
    a = 10   # Static variable

    def __init__(self):
        Test.a = 100   # Updating static variable inside constructor

# Create object
t = Test()

# Print updated static variable
print(Test.a)

# Output : 100


Where Can We Modify Static Variables in Python?.

class Test:
    a = 10  # Static (class-level) variable

    def __init__(self):
        Test.a = 888  # Modify inside constructor

    def m1(self):
        Test.a = 10000  # Modify inside instance method

    @classmethod
    def m2(cls):
        Test.a = 9090   # Modify using class name
        cls.a = 2222    # Modify using cls

    @staticmethod
    def m3():
        Test.a = 22122  # Modify inside static method

# Modify from constructor
t = Test()
print(Test.a)    # 888

# Modify from instance method
t.m1()
print(Test.a)    # 10000

# Modify from class method
t.m2()
print(Test.a)    # 2222

# Modify from static method
t.m3()
print(Test.a)    # 22122

# Modify directly using class name
Test.a = 44322
print(Test.a)    # 44322


Understanding Class, Instance, and Local Variables in Python.

class Test:
    a = 10  # Class variable

    def __init__(self):
        Test.a = 1000     # Modify class variable
        a = 100           # Local variable (only in constructor)
        self.a = 90       # Instance variable
        print(self.a)     # Output: 90 (instance variable)
        print(Test.a)     # Output: 1000 (modified class variable)
        # print(b)        # ❌ Will raise NameError (b not defined here)

    def m1(self):
        b = 10            # Local variable
        print(b)          # Output: 10

# Create object and call methods
t = Test()
t.m1()

# Output :
90
1000
10


Difference Between Class and Instance Variables (Shadowing Example).

class Test:
    x = 10  # Class variable

    def __init__(self):
        self.y = 20  # Instance variable

# Create two objects
t1 = Test()
t2 = Test()

# Initial values
print(t1.x, t1.y)  # 10 20
print(t2.x, t2.y)  # 10 20

# Modify values
t1.x = 888         # Creates a new instance variable 'x' for t1
t2.y = 1000        # Modifies instance variable 'y' for t2

# Values after modification
print(t1.x, t1.y)  # 888 20 (t1.x is now instance-level)
print(t2.x, t2.y)  # 10 1000 (t2.x still refers to class-level)

# Output :
10 20
10 20
888 20
10 1000


Important Note :

When you assign t1.x = 888, Python does NOT change the class variable.

It creates a new instance variable x only for t1, which shadows the class variable.

You can confirm this by printing :

print(Test.__dict__)  # Class variables only
print(t1.__dict__)    # Instance variables of t1
print(t2.__dict__)    # Instance variables of t2


How Instance Methods Can Shadow Class Variables in Python.

class Test:
    a = 10  # Class variable

    def __init__(self):
        self.b = 20  # Instance variable

    def m1(self):
        self.a = 888  # Creates an instance variable named 'a'
        self.b = 999  # Updates the instance variable 'b'

# Create two instances
t1 = Test()
t2 = Test()

# Modify t1 only
t1.m1()

# Print values for t1
print(t1.a, t1.b)  # Output: 888 999

# Print values for t2 (unaffected)
print(t2.a, t2.b)  # Output: 10 20

# Output :
888 999
10 20


Modifying Class Variables Using Class Methods in Python (With Instance Variable Restriction).

class Test:
    a = 10  # Class variable

    def __init__(self):
        self.b = 20  # Instance variable

    @classmethod
    def m1(cls):
        cls.a = 888  # Modify class variable
        # cls.b = 999  # Error: 'b' is an instance variable, not accessible via cls

# Create two objects
t1 = Test()
t2 = Test()

# Call class method using t1
t1.m1()

# Access variables
print(t1.a, t1.b)  # 888 20
print(t2.a, t2.b)  # 888 20
print(Test.a)      # 888

# print(Test.b)    # ❌ This would raise AttributeError

# Output :
888 20
888 20
888


Store and Display Multiple Student Records Using Setters and Getters.

class Student:
    def setName(self, name):
        self.name = name  # Set instance variable 'name'

    def getName(self):
        return self.name  # Get instance variable 'name'

    def setMarks(self, marks):
        self.marks = marks  # Set instance variable 'marks'

    def getMarks(self):
        return self.marks  # Get instance variable 'marks'


# Create an empty list to store student objects
students_list = []

# Input: Number of students
num_students = int(input("Enter the number of students: "))

# Input student details and store objects in the list
for i in range(num_students):
    s = Student()
    name = input(f"Enter name of student {i+1}: ")
    marks = input("Enter marks: ")
    s.setName(name)
    s.setMarks(marks)
    students_list.append(s)

# Display all student records
for s in students_list:
    print("\nStudent Name:", s.getName())
    print("Student Marks:", s.getMarks())

# Output :
Enter the number of students: 2
Enter name of student 1: Ram
Enter marks: 87
Enter name of student 2: Shyam
Enter marks: 90

Student Name: Ram
Student Marks: 87

Student Name: Shyam
Student Marks: 90


Inner Classes (Class Inside a Class).

class Outer:
    def __init__(self):
        print("Outer class object creation...")

    class Inner:
        def __init__(self):
            print("Inner class object creation...")

        def m1(self):
            print("Inner Class Method")

# Create outer class object
o = Outer()

# Create inner class object via outer class object
i = o.Inner()
i.m1()

# Output :
Outer class object creation...
Inner class object creation...
Inner Class Method


What is Garbage Collector (GC)?

Garbage Collector (GC) is a part of Python’s memory management system, and it's managed by the Python Virtual Machine (PVM).

GC is responsible for automatically deleting unused (unreachable) objects from memory to free up space.


Key Points :


Example :

import gc

class Test:
    def __del__(self):
        print("Destructor called, object deleted.")

t1 = Test()
t2 = t1
del t1
del t2

# Forcing garbage collection manually
gc.collect()

# Output : Destructor called, object deleted.