CSE3011 – Python Programming
UNIT 3
Object-Oriented Programming in Python
3.1 Introduction to Object-Oriented Programming
Python is an object-oriented language. Before the advent of object-oriented programming (OOP),
programs were written in a purely procedural style — a series of functions and instructions executed
one after another. While this worked for small programs, it became increasingly difficult to manage as
programs grew in complexity.
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design
around data (objects) rather than functions and logic. It models real-world entities as software objects
that have both attributes (data/state) and behaviors (methods/functions). This approach brings natural
structure to programs and makes code more reusable, extensible, and maintainable.
Python is a fully object-oriented language. Everything in Python is an object — integers, strings, lists,
functions, and even classes themselves are all objects. We have already been using OOP since we
started with Python: int, str, float, list, dict — these are all built-in classes, and every value is an instance
(object) of one of these classes.
3.1.1 The Four Pillars of OOP
Pillar Description Real-World Analogy
Encapsulation Bundling data (attributes) and A capsule: medicine inside a
methods together inside a sealed container
class; restricting direct access
to internal data
Inheritance Creating a new class from an A child inheriting traits from a
existing class, inheriting its parent
attributes and methods
Polymorphism Ability to process objects of A single remote controlling
different types through a single different devices
uniform interface
Abstraction Hiding implementation Driving a car without knowing
complexity; showing only engine internals
essential features to the user
3.1.2 Key OOP Terminology
Class: A blueprint or template that defines the structure (attributes) and behavior (methods) of its
objects. Example: 'Student' class defines what every student object will look like.
Object: An instance (a specific realization) of a class. Example: 'Alice', 'Bob', 'Carol' are three
different Student objects.
Attribute: A variable that belongs to a class or object. It stores the state/data. Example:
[Link], [Link].
Method: A function defined inside a class. It defines the behavior of an object. Example:
[Link](), student.calculate_grade().
Instance: Another word for object — a specific realization of a class created in memory.
Instantiation: The process of creating an object from a class. Example: s1 = Student() creates an
instance.
Constructor: A special method (__init__) automatically called when an object is created to
initialize its attributes.
Destructor: A special method (__del__) automatically called when an object is about to be
destroyed.
Everything is an Object: In Python, every value is an object of some class: type(5) → <class 'int'>,
type('hello') → <class 'str'>, type([1,2,3]) → <class 'list'>. Even functions are objects! This is what
makes Python a pure object-oriented language.
3.2 Defining Classes in Python
A class is a user-defined blueprint that describes what attributes (data) and methods (behavior) an
object of that type will have. Once the class is defined, we can create multiple objects (instances) from
it — each object has its own copy of the class attributes.
A class declaration only creates a template — it does not create any actual object in memory. Think of
a class as a cookie cutter and objects as the cookies made from it. The cookie cutter (class) defines
the shape; each cookie (object) is a separate, independent creation.
3.2.1 Class Syntax
class ClassName:
# Class body
attribute1 = value # Class attribute
attribute2 = value
def method1(self): # Instance method
statement(s)
def method2(self, param):
statement(s)
Key rules:
1. The class keyword begins every class definition.
2. ClassName follows the CapWords convention (PascalCase): Student, BankAccount, Circle.
3. The class body is an indented block (all contents must be indented).
4. The pass statement can be used for an empty class body.
5. Class attributes are defined directly in the class body — they are shared by all instances.
6. Methods are defined using def inside the class; the first parameter must always be self.
Program 1: Simplest Class
class Demo:
pass # Empty class body
D1 = Demo() # Creating an object (instantiation)
print(D1) # <__main__.Demo object at 0x029B3150>
The output shows the class name and memory address. The creation of a new object is called
instantiation. In Python, there is no 'new' keyword as in Java or C++ — simply calling the class name
creates an object.
Program 2: Class with a print statement
class MyFirstProgram:
print('Welcome to Object-oriented Programming')
C = MyFirstProgram() # Instance of class
print(C)
Output
Welcome to Object-oriented Programming
<__main__.MyFirstProgram object at 0x028B6C90>
3.2.2 Adding Attributes to a Class
Attributes are variables that store the state of an object. They are defined inside the class body. When
an object is created, each object gets its own independent copy of the instance attributes.
Program 3: Class with attributes
class Rectangle:
length = 0 # Class attribute
breadth = 0 # Class attribute
R1 = Rectangle() # Create object
print([Link]) # Access attribute: 0
print([Link]) # Access attribute: 0
3.2.3 Accessing and Assigning Attribute Values
The dot (.) operator is used both to access and to assign values to the attributes of an object:
# Syntax to access an attribute:
<object>.<attribute>
# Syntax to assign a value to an attribute:
<object>.<attribute> = <value>
Program 4: Rectangle area with attribute assignment
class Rectangle:
length = 0
breadth = 0
R1 = Rectangle()
print('Initial values:')
print('Length =', [Link]) # 0
print('Breadth =', [Link]) # 0
print('Area =', [Link] * [Link]) # 0
[Link] = 20 # Assign new value
[Link] = 30 # Assign new value
print('After reassigning:')
print('Length =', [Link]) # 20
print('Breadth =', [Link]) # 30
print('Area =', [Link] * [Link]) # 600
Output
Initial values:
Length = 0 Breadth = 0 Area = 0
After reassigning:
Length = 20 Breadth = 30 Area = 600
3.3 Instance Methods and the Self Parameter
Methods are functions defined inside a class. They define the behaviors of objects. The most important
feature of instance methods in Python is that the first parameter of every method must be self. The self
parameter is a reference to the object on which the method is being called. It is analogous to the 'this'
pointer in C++ and the 'this' reference in Java.
3.3.1 Adding Methods to a Class
class ClassName:
instance_variable = value
def method_name(self, parameter_list): # parameter_list is optional
block_of_statements
self Parameter Rules: 1) The first parameter of every instance method must be self. 2) self refers to
the current object itself. 3) Although 'self' can technically be renamed, using 'self' is a strong
convention and is expected by all Python programmers.
Program 5: Method in a class
class MethodDemo:
def Display_Message(self):
print('Welcome to Python Programming')
ob1 = MethodDemo() # Create object
ob1.Display_Message() # Call method
Output
Welcome to Python Programming
Program 6: Method with parameters (Circle area)
import math
class Circle:
def Calc_Area(self, radius):
print('radius =', radius)
return [Link] * radius ** 2
ob1 = Circle()
print('Area of circle is', ob1.Calc_Area(5))
Output
radius = 5
Area of circle is 78.53981633974483
Important: Even though the method Calc_Area has two parameters (self and radius), only ONE
argument (the radius value) is passed when calling it. Python automatically passes the object itself as
the 'self' argument.
3.3.2 The Self Parameter with Instance Variables
The self parameter serves double duty: it references the object itself AND allows access to the object's
instance variables from within methods. When a local variable in a method has the same name as an
instance variable, the local variable 'hides' (shadows) the instance variable. Use self.variable_name to
explicitly access the instance variable.
Program 7: Variable hiding problem
class Prac:
x = 5 # Instance variable
def disp(self, x):
x = 30 # Local variable (shadows instance variable)
print('Local variable x is', x)
print('Instance variable x is', x) # Both print 30!
ob = Prac()
[Link](50)
Output — Problem: both show 30
Local variable x is 30
Instance variable x is 30
Program 8: Solution using self.x
class Prac:
x = 5
def disp(self, x):
x = 30
print('Local variable x is', x)
print('Instance variable x is', self.x) # self.x = 5
ob = Prac()
[Link](50)
Output — Correct
Local variable x is 30
Instance variable x is 5
3.3.3 Calling a Method from Another Method Using self
The self parameter also enables one method inside a class to call another method in the same class.
This allows methods to be composed and reused within the class.
Program 9: One method calling another
class Self_Demo:
def Method_A(self):
print('In Method A')
print('wow got called from A!!!')
def Method_B(self):
print('In Method B calling Method A')
self.Method_A() # Call Method_A from Method_B
Q = Self_Demo()
Q.Method_B()
Output
In Method B calling Method A
In Method A
wow got called from A!!!
3.3.4 Displaying Class Attributes and Methods
Python provides two ways to inspect the attributes and methods of a class:
7. dir(class_or_object): Returns a sorted list of all attributes and methods, including special
dunder methods.
8. ClassName.__dict__: Returns a dictionary of the class's own attributes and methods (without
inherited ones).
class DisplayDemo:
Name = ''
Age = 0
def read(self):
pass
# Using dir()
print(dir(DisplayDemo))
# ['Age', 'Name', '__class__', '__delattr__', ...., 'read']
# Using __dict__
print(DisplayDemo.__dict__)
# mappingproxy({'Name': '', 'Age': 0, 'read': <function...>})
3.4 The __init__ Method (Constructor)
The __init__ method is Python's constructor. It is a special method (dunder method — double
underscore on both sides) that Python automatically calls immediately when a new object is created
from a class. Its primary purpose is to initialize the instance variables of the newly created object with
appropriate starting values.
The double underscores around 'init' are part of Python's convention for special methods. These are
also called 'dunder methods' (from 'double underscore') or 'magic methods'. They are automatically
invoked by Python in response to specific operations.
3.4.1 Syntax of __init__
class ClassName:
def __init__(self): # No parameters version
self.attribute1 = value1
self.attribute2 = value2
# OR with parameters:
def __init__(self, param1, param2): # Parametrized version
self.attribute1 = param1
self.attribute2 = param2
The self parameter inside __init__ refers to the newly created object. When we write [Link] =
value, we are setting an attribute on that specific object.
Program 10: Basic __init__ method
class Circle:
def __init__(self, pi):
[Link] = pi # Initialize instance variable
def calc_area(self, radius):
return [Link] * radius ** 2
C1 = Circle(3.14) # 3.14 passed to __init__ as pi
print('The area of Circle is', C1.calc_area(5))
Output
The area of Circle is 78.5
Program 11: __init__ with default initialization
class Circle:
pi = 0
radius = 0
def __init__(self):
[Link] = 3.14 # Override class attribute with instance
attribute
[Link] = 5
def calc_area(self):
print('Radius =', [Link])
return [Link] * [Link] ** 2
C1 = Circle()
print('The area of Circle is', C1.calc_area())
Program 12: Student class with __init__
class Student:
def __init__(self, name, roll, marks):
[Link] = name
[Link] = roll
[Link] = marks
def display(self):
print('Name:', [Link])
print('Roll:', [Link])
print('Marks:', [Link])
def get_grade(self):
if [Link] >= 90: return 'A+'
elif [Link] >= 80: return 'A'
elif [Link] >= 60: return 'B'
else: return 'F'
s1 = Student('Alice', 101, 92)
s2 = Student('Bob', 102, 75)
[Link]()
print('Grade:', s1.get_grade())
Output
Name: Alice Roll: 101 Marks: 92
Grade: A+
Constructor Note: Python does not have separate constructors and initializers like C++. The
__init__ method serves as the initializer. There is technically a __new__ method that creates the
object, but programmers typically only override __init__. Note that __init__ never returns a value — it
implicitly returns None.
3.4.2 Passing an Object as Parameter to a Method
Just as primitive values can be passed to methods, entire objects can also be passed as parameters.
This is used to compare objects, combine objects, or perform operations that involve two objects of the
same class.
Program 13: Passing object as parameter
class Test:
def __init__(self, x, y):
self.a = x
self.b = y
def equals(self, obj): # obj is another Test object
if obj.a == self.a and obj.b == self.b:
return True
else:
return False
Obj1 = Test(10, 20)
Obj2 = Test(10, 20)
Obj3 = Test(12, 90)
print('Obj1 == Obj2:', [Link](Obj2)) # True
print('Obj1 == Obj3:', [Link](Obj3)) # False
Output
Obj1 == Obj2: True
Obj1 == Obj3: False
3.4.3 The __del__ Destructor Method
The __del__ method is the destructor. Python automatically calls it just before an object is destroyed
(garbage collected). Python uses reference counting for memory management — an object is
destroyed when no variable holds a reference to it. The __del__ method executes only when the
reference count drops to zero (i.e., ALL references to the object are removed).
Program 14: Destructor demonstration
class Destructor_Demo:
def __init__(self):
print('Welcome (Object Created)')
def __del__(self):
print('Destructor Executed (Object Destroyed)')
Ob1 = Destructor_Demo() # __init__ called
Ob2 = Ob1 # Ob2 is alias for Ob1
Ob3 = Ob1 # Ob3 is alias for Ob1
print('Id of Ob1:', id(Ob1))
print('Id of Ob2:', id(Ob2)) # Same as Ob1
print('Id of Ob3:', id(Ob3)) # Same as Ob1
del Ob2 # Reference count: 3 → 2
del Ob1 # Reference count: 2 → 1
del Ob3 # Reference count: 1 → 0 → __del__ called!
Output
Welcome (Object Created)
Id of Ob1: 47364272
Id of Ob2: 47364272
Id of Ob3: 47364272
Destructor Executed (Object Destroyed)
Destructor Rule: The __del__ destructor is invoked only when the LAST reference to an object is
deleted. If three variables (Ob1, Ob2, Ob3) all point to the same object, __del__ is called only after all
three are deleted. __del__ is called exactly once per object.
3.5 Access Specification and Encapsulation
Encapsulation is the OOP principle of bundling data and methods together inside a class and restricting
direct access to some components. This prevents external code from accidentally modifying internal
state and enforces a controlled interface for interaction with objects.
Unlike Java and C++ which have explicit keywords (public, protected, private), Python uses a naming
convention to indicate the intended accessibility of class members. There are no strict access modifiers
— Python trusts the programmer to respect the convention.
3.5.1 Access Levels in Python
Access Level Syntax Convention Accessible From
Public attribute_name No underscore prefix Everywhere — inside
class, subclasses,
outside class
Protected _attribute_name Single underscore Intended for class and
prefix subclasses only
(convention, not
enforced)
Private __attribute_name Double underscore Only within the class
prefix (name mangling
applied)
Program 15: Access specification demonstration
class Person:
def __init__(self):
[Link] = 'Bill Gates' # Public attribute
self._age = 65 # Protected attribute
self.__BankAccNo = 10101 # Private attribute
def Display(self):
print('Name:', [Link])
print('Bank Account No:', self.__BankAccNo) # OK inside class
P = Person()
print([Link]) # OK — public
print(P._age) # Works but convention says don't
[Link]() # OK — calls method
print(P.__BankAccNo) # AttributeError! Cannot access private
Output
Bill Gates
65
Name: Bill Gates
Bank Account No: 10101
AttributeError: 'Person' object has no attribute '__BankAccNo'
Name Mangling: Python does not actually delete private attributes — it renames them using name
mangling. An attribute __BankAccNo in class Person becomes _Person__BankAccNo internally. You
CAN still access it as P._Person__BankAccNo but this is strongly discouraged.
3.5.2 Why Encapsulation Matters
Encapsulation serves several important purposes in software design:
• Data Protection: Prevents accidental modification of sensitive data (like a bank account
balance or password hash).
• Controlled Access: Forces users to interact with the class through well-defined methods
(getters and setters) rather than directly with raw data.
• Implementation Independence: The internal implementation of a class can be changed without
affecting the code that uses it, as long as the public interface remains the same.
• Reduced Coupling: Classes are less dependent on each other's internal workings, making the
system easier to modify and maintain.
Program: Encapsulation with getter/setter methods
class BankAccount:
def __init__(self, owner, balance):
self.__owner = owner
self.__balance = balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f'Deposited {amount}. New balance: {self.__balance}')
else:
print('Invalid deposit amount')
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
print(f'Withdrew {amount}. New balance: {self.__balance}')
else:
print('Insufficient funds or invalid amount')
def get_balance(self): # Getter method
return self.__balance
acc = BankAccount('Alice', 1000)
[Link](500)
[Link](200)
print('Balance:', acc.get_balance())
3.6 Data Modeling and Persistent Storage of Objects
3.6.1 Data Modeling with Classes
Data modeling with classes means designing classes that accurately represent real-world entities and
their relationships. A well-designed class models the essential attributes and behaviors of the entity it
represents. The class serves as a data model — a structured representation of the data.
For example, modeling a library system requires classes like Book (with attributes: title, author, ISBN,
copies), Member (name, ID, borrowed_books), and Library (with methods to borrow and return books).
The relationships between these classes are expressed through attributes (a Library has-a list of
Books).
Example: Modeling a bank account system
class Account:
def __init__(self, acc_no, owner, balance=0):
self.acc_no = acc_no
[Link] = owner
self.__balance = balance
[Link] = []
def deposit(self, amt):
self.__balance += amt
[Link](f'+{amt}')
def withdraw(self, amt):
if amt <= self.__balance:
self.__balance -= amt
[Link](f'-{amt}')
def display(self):
print(f'Account: {self.acc_no} | Owner: {[Link]}')
print(f'Balance: {self.__balance}')
print(f'Transactions: {[Link]}')
3.6.2 Persistent Storage of Objects (pickle module)
By default, Python objects exist only in RAM and are lost when the program terminates. To make
objects persist across program executions (i.e., save them to disk and reload them later), Python
provides the pickle module. Pickling is the process of serializing an object — converting it into a byte
stream that can be written to a file. Unpickling is the reverse: reading the byte stream and reconstructing
the original object.
Saving objects to file using pickle
import pickle
class Student:
def __init__(self, name, marks):
[Link] = name
[Link] = marks
def __str__(self):
return f'Student({[Link]}, {[Link]})'
# Serialize (save) objects to file
students = [Student('Alice', 95), Student('Bob', 82)]
with open('[Link]', 'wb') as f:
[Link](students, f)
print('Students saved to disk')
Loading objects from file
import pickle
# Deserialize (load) objects from file
with open('[Link]', 'rb') as f:
loaded = [Link](f)
for s in loaded:
print(s)
Output
Students saved to disk
Student(Alice, 95)
Student(Bob, 82)
pickle Security Note: The pickle module should not be used to deserialize data from untrusted
sources, as malicious pickle data can execute arbitrary code during unpickling. For safe serialization of
simple data, use JSON instead.
3.6.3 Class Membership Tests
Python provides the isinstance() and issubclass() functions to check object types at runtime. These are
essential for writing robust, type-aware code.
Function Purpose Syntax Returns
isinstance(obj, Class) Check if obj is an isinstance(Ob1, A) True or False
instance of Class (or
its subclass)
issubclass(Sub, Base) Check if Sub is a issubclass(B, A) True or False
subclass of Base
type(obj) Get the exact type of type(42) <class 'int'>
obj
isinstance() examples
class A: pass
class B: pass
class C: pass
Ob1 = A()
Ob2 = B()
print(isinstance(Ob1, A)) # True
print(isinstance(Ob1, B)) # False
print(isinstance(Ob2, B)) # True
print(isinstance(Ob2, C)) # False
3.7 Inheritance
Inheritance is one of the most powerful and fundamental features of object-oriented programming. It
allows a new class (called the subclass, derived class, or child class) to acquire the attributes and
methods of an existing class (called the superclass, base class, or parent class). The subclass inherits
everything from the base class and can add new attributes and methods or modify (override) inherited
ones.
Inheritance models 'is-a' relationships in the real world: a Car IS-A Vehicle, a Dog IS-AN Animal, a
SavingsAccount IS-A BankAccount. This hierarchy reduces code duplication and makes programs
more organized and extensible.
3.7.1 Syntax of Inheritance
# Single inheritance
class DerivedClass(BaseClass):
body_of_derived_class
# Multiple inheritance
class DerivedClass(BaseClass1, BaseClass2, ...):
body_of_derived_class
The base class name is placed in parentheses after the derived class name. Every class in Python that
does not explicitly specify a base class automatically inherits from the built-in object class.
3.7.2 Types of Inheritance
Type Description Syntax Pattern Diagram
Single One base class → class B(A): A→B
One derived class
Multilevel Derived class itself class B(A): class C(B): A→B→C
becomes base class
for another
Multiple Multiple base classes class C(A, B): A+B→C
→ One derived class
Hierarchical One base class → class B(A): class C(A): A → B and A → C
Multiple derived
classes
3.7.3 Single Inheritance
In single inheritance, one base class is used to create one derived class. The derived class inherits all
attributes and methods of the base class and can add new ones.
Program 16: Basic single inheritance
class A:
print('Hello I am in Base Class')
class B(A):
print('Wow!! I am in Derived class')
ob2 = A()
Output
Hello I am in Base Class
Wow!! I am in Derived class
Program 17: Subclass accessing base class methods and attributes
class A:
i = 0
j = 0
def Showij(self):
print('i =', self.i, 'j =', self.j)
class B(A): # B inherits A
k = 0
def Showijk(self):
print('i =', self.i, 'j =', self.j, 'k =', self.k)
def sum(self):
print('i + j + k =', self.i + self.j + self.k)
Ob1 = A()
Ob2 = B() # B object can access A's attributes
Ob1.i = 100; Ob1.j = 200
Ob2.i = 100; Ob2.j = 200; Ob2.k = 300
[Link]() # Calling inherited method from A
[Link]()
[Link]()
Output
i = 100 j = 200
i = 100 j = 200 k = 300
i + j + k = 600
Program 18: Practical single inheritance — Point and New_Point
class Point: # Base Class
def Set_Coordinates(self, X, Y):
self.X = X
self.Y = Y
class New_Point(Point): # Derived Class
def draw(self):
print('Locate Point X =', self.X, 'On X axis')
print('Locate Point Y =', self.Y, 'On Y axis')
P = New_Point()
P.Set_Coordinates(10, 20) # Inherited method
[Link]() # Own method
Output
Locate Point X = 10 On X axis
Locate Point Y = 20 On Y axis
3.7.4 Multilevel Inheritance
In multilevel inheritance, a class is derived from another derived class, creating a chain of inheritance.
The derived class acts as both a child (of its parent) and a parent (of its child). Attributes and methods
are inherited across all levels.
Program 19: Multilevel inheritance — Person → Employee data
class A: # Base Class
name = ' '
age = 0
class B(A): # Derived from A
height = ' '
class C(B): # Derived from B (which is derived from A)
weight = 0
def Read(self):
[Link] = input('Enter Name: ')
[Link] = int(input('Enter Age: '))
[Link] = input('Enter Height: ')
[Link] = int(input('Enter Weight: '))
def Display(self):
print('Name:', [Link])
print('Age:', [Link])
print('Height:', [Link])
print('Weight:', [Link])
B1 = C() # Instance of deepest class
[Link]()
[Link]()
Output
Name: Amit Age: 25 Height: 5.7ft Weight: 60
3.7.5 Multiple Inheritance
In multiple inheritance, one derived class inherits from two or more base classes simultaneously. The
derived class gets the combined features of all its base classes. Python supports multiple inheritance
directly, unlike Java (which uses interfaces instead).
Program 20: Multiple inheritance
class A:
a = 0
class B:
b = 0
class C(A, B): # C inherits from both A and B
c = 0
def Read(self):
self.a = int(input('Enter a: '))
self.b = int(input('Enter b: '))
self.c = int(input('Enter c: '))
def display(self):
print('a =', self.a, 'b =', self.b, 'c =', self.c)
Ob1 = C()
[Link]()
[Link]()
Output
a = 10 b = 20 c = 30
3.7.6 The Object Class
Every class in Python is ultimately derived from the built-in object class. If no base class is specified
when defining a class, Python automatically makes it inherit from object. This means all classes share
a set of default methods like __str__, __repr__, __eq__, __hash__, etc.
class MyClass: # Equivalent to:
pass # class MyClass(object): pass
print(issubclass(MyClass, object)) # True
print(isinstance(42, object)) # True (everything is an object)
3.8 The super() Function
The super() function provides a way for a subclass to call methods from its superclass. It is most
commonly used to call the superclass constructor (__init__) from the subclass constructor. This is
important for ensuring that the superclass is properly initialized before the subclass adds its own
initialization.
Without super(), a subclass would need to duplicate all the initialization code from the parent class,
violating the DRY (Don't Repeat Yourself) principle. With super(), the subclass can simply delegate to
the parent's __init__ for the shared initialization.
3.8.1 Syntax
super().__init__(parameters) # Python 3 syntax
super(DerivedClass, self).__init__(...) # Python 2 syntax
3.8.2 Without super() — Code Duplication Problem
Without super — duplicate code
class Demo:
def __init__(self, A, B, C):
self.a = A; self.b = B; self.c = C
def display(self):
print(self.a, self.b, self.c)
class NewDemo(Demo):
def __init__(self, A, B, C, D):
self.a = A; self.b = B; self.c = C # Duplicate code!
self.d = D
def display(self):
print(self.a, self.b, self.c, self.d)
3.8.3 With super() — Elegant Solution
With super — clean code
class Demo:
def __init__(self, A, B, C):
self.a = A; self.b = B; self.c = C
def display(self):
print(self.a, self.b, self.c)
class NewDemo(Demo):
def __init__(self, A, B, C, D):
self.d = D
super().__init__(A, B, C) # Delegate to parent's __init__
def display(self):
print(self.a, self.b, self.c, self.d)
B1 = Demo(100, 200, 300)
print('Contents of Base Class:')
[Link]()
D1 = NewDemo(10, 20, 30, 40)
print('Contents of Derived Class:')
[Link]()
Output
Contents of Base Class:
100 200 300
Contents of Derived Class:
10 20 30 40
super() with Multiple Inheritance: super() is especially valuable in multiple inheritance because it
uses the MRO (Method Resolution Order) to determine which parent's method to call. This prevents
methods from being called multiple times in diamond inheritance patterns.
3.9 Polymorphism and Method Overriding
Polymorphism (from Greek: poly = many, morphism = forms) is the ability of different objects to respond
to the same method call in different, type-appropriate ways. In OOP terms, a single interface (method
name) can work with objects of different classes. Python achieves polymorphism through method
overriding and duck typing.
3.9.1 Method Overriding
Method overriding occurs when a method in a subclass has exactly the same name and signature as
a method in its superclass. When the method is called on a subclass object, the subclass version
executes (overrides the parent version). This is the primary mechanism for implementing polymorphism
in Python.
Key characteristics of method overriding:
• The method in the subclass must have the exact same name as the one in the superclass.
• The subclass method takes precedence over (hides) the superclass method.
• The overriding method can still call the original superclass method using super().
Program 21: Basic method overriding
class A:
i = 0
def display(self):
print('I am in Super Class (A)')
class B(A):
i = 0
def display(self): # Overrides A's display()
print('I am in Sub Class (B)')
D1 = B()
[Link]() # B's display() is called, not A's
Output
I am in Sub Class (B)
Program 22: Calling overridden method using super()
class A:
def display(self):
print('I am in Super Class')
class B(A):
def display(self):
print('I am in Sub Class')
super().display() # Also call A's display
D1 = B()
[Link]()
Output
I am in Sub Class
I am in Super Class
3.9.2 Polymorphism in Action
The true power of polymorphism is the ability to write code that works with objects of any class in a
hierarchy, as long as they share the same method interface. This allows programs to be extended
without modifying existing code.
Program 23: Polymorphism with Shape hierarchy
import math
class Shape:
def area(self):
return 0
def describe(self):
print(f'Area of {type(self).__name__}: {[Link]():.2f}')
class Circle(Shape):
def __init__(self, radius):
[Link] = radius
def area(self): # Override
return [Link] * [Link] ** 2
class Rectangle(Shape):
def __init__(self, length, breadth):
[Link] = length
[Link] = breadth
def area(self): # Override
return [Link] * [Link]
class Triangle(Shape):
def __init__(self, base, height):
[Link] = base
[Link] = height
def area(self): # Override
return 0.5 * [Link] * [Link]
# Polymorphic behavior
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
[Link]() # Same call, different behavior
Output
Area of Circle: 78.54
Area of Rectangle: 24.00
Area of Triangle: 12.00
3.9.3 Method Overloading in Python
Method overloading (having multiple methods with the same name but different parameter
types/counts) is NOT directly supported in Python. Python simply recognizes the last definition of a
method with a given name — earlier definitions are replaced. However, Python achieves similar
functionality using default arguments or variable-length argument lists.
Method overloading with default arguments
class methodOverloading:
def greeting(self, name=None):
if name is not None:
print('Welcome ' + name)
else:
print('Welcome')
obj = methodOverloading()
[Link]() # Welcome
[Link]('Donald Trump') # Welcome Donald Trump
Method Overloading vs Overriding: Method Overloading: Same method name, different
parameters — NOT directly supported in Python (only last definition counts). Method Overriding: Same
method name AND same parameters in subclass — FULLY supported and is the basis of
polymorphism.
3.10 Operator Overloading
Operator overloading allows programmers to define custom behavior for standard Python operators (+,
-, *, /, ==, <, etc.) when they are applied to objects of user-defined classes. This makes user-defined
types behave like built-in types.
When Python encounters an expression like x + y, it internally converts it to a method call: x.__add__(y).
To overload the + operator for a class, the programmer implements the __add__ method. These special
methods are called 'dunder methods' (double underscore) or 'magic methods'.
3.10.1 Special Methods for Arithmetic Operators
Operation Special Method Invoked When
x+y __add__(self, other) x+y
x-y __sub__(self, other) x-y
x*y __mul__(self, other) x*y
x/y __truediv__(self, other) x/y
x // y __floordiv__(self, other) x // y
x%y __mod__(self, other) x%y
Operation Special Method Invoked When
x ** y __pow__(self, other) x ** y
-x __neg__(self) -x (unary negation)
Program 24: Overloading the + operator
class OprOverloadingDemo:
def __init__(self, X):
self.X = X
def __add__(self, other):
print('The value of Ob1 =', self.X)
print('The value of Ob2 =', other.X)
return self.X + other.X
Ob1 = OprOverloadingDemo(20)
Ob2 = OprOverloadingDemo(30)
Ob3 = Ob1 + Ob2 # Python calls Ob1.__add__(Ob2)
print('Addition:', Ob3)
Output
The value of Ob1 = 20
The value of Ob2 = 30
Addition: 50
3.10.2 Special Methods for Comparison Operators
Operation Special Method Description
x == y __eq__(self, other) Is x equal to y?
x != y __ne__(self, other) Is x not equal to y?
x<y __lt__(self, other) Is x less than y?
x <= y __le__(self, other) Is x less than or equal to y?
x>y __gt__(self, other) Is x greater than y?
x >= y __ge__(self, other) Is x greater than or equal to y?
Program 25: Overloading comparison operators
class CmpOprDemo:
def __init__(self, X):
self.X = X
def __lt__(self, other):
return self.X < other.X
def __gt__(self, other):
return self.X > other.X
def __le__(self, other):
return self.X <= other.X
def __eq__(self, other):
return self.X == other.X
Ob1 = CmpOprDemo(20)
Ob2 = CmpOprDemo(30)
print('Ob1 < Ob2:', Ob1 < Ob2) # True
print('Ob1 > Ob2:', Ob1 > Ob2) # False
print('Ob1 <= Ob2:', Ob1 <= Ob2) # True
print('Ob1 == Ob2:', Ob1 == Ob2) # False
3.10.3 Special Methods for Built-in Functions
Function Special Method Description
str(x) __str__(self) Human-readable string
representation
repr(x) __repr__(self) Developer-facing string
representation
len(x) __len__(self) Length of the object
abs(x) __abs__(self) Absolute value
float(x) __float__(self) Float conversion
int(x) __int__(self) Integer conversion
hash(x) __hash__(self) Hash value for use in sets/dicts
bool(x) __bool__(self) Boolean value
Program 26: __str__ and __len__ overloading
class Student:
def __init__(self, name, marks):
[Link] = name
[Link] = marks
def __str__(self):
return f'Student({[Link]}, marks={[Link]})'
def __len__(self):
return len([Link]) # Returns length of name
s = Student('Alice', 92)
print(s) # Calls __str__: Student(Alice, marks=92)
print(len(s)) # Calls __len__: 5
3.10.4 Reference Equality vs. Object Equality
Python distinguishes between two types of equality:
9. Reference Equality (identity): Do two variables point to the SAME object in memory? Use the
'is' operator. Checks if id(a) == id(b).
10. Object Equality (value): Do two objects have the same VALUE? Use the == operator. Calls
__eq__ method.
Example
Ob1 = 50
Ob2 = 60
Ob3 = Ob1
Ob4 = 50
print(Ob1 is Ob2) # False — different memory locations
print(Ob3 is Ob1) # True — Ob3 references same object as Ob1
print(Ob1 == Ob4) # True — same VALUE (50 == 50)
print(id(Ob1), id(Ob3)) # Same address
print(id(Ob1), id(Ob2)) # Different addresses
3.11 Abstract Classes
An abstract class is a class that cannot be instantiated directly — it is meant to serve as a base class
for other classes. Abstract classes define a common interface (a set of methods that all subclasses
must implement), but leave the actual implementation to the subclasses. This enforces a contract: any
subclass MUST provide implementations for all abstract methods.
Python provides abstract classes through the abc (Abstract Base Class) module. A class is made
abstract by inheriting from ABC and decorating methods with @abstractmethod.
3.11.1 Creating Abstract Classes
from abc import ABC, abstractmethod
class AbstractShape(ABC): # Abstract class — cannot be instantiated
@abstractmethod
def area(self): # Abstract method — must be implemented
pass
@abstractmethod
def perimeter(self): # Another abstract method
pass
def describe(self): # Concrete method — can be inherited as-is
print(f'I am a {type(self).__name__}')
print(f'Area: {[Link]():.2f}')
Subclasses must implement abstract methods
import math
class Circle(AbstractShape):
def __init__(self, r):
self.r = r
def area(self):
return [Link] * self.r ** 2
def perimeter(self):
return 2 * [Link] * self.r
class Rectangle(AbstractShape):
def __init__(self, l, b):
self.l = l; self.b = b
def area(self):
return self.l * self.b
def perimeter(self):
return 2 * (self.l + self.b)
# Testing
c = Circle(5)
[Link]()
r = Rectangle(4, 6)
[Link]()
# Attempting to instantiate abstract class raises TypeError
# s = AbstractShape() # TypeError: Can't instantiate abstract class
Output
I am a Circle
Area: 78.54
I am a Rectangle
Area: 24.00
Abstract Class Purpose: Abstract classes enforce a common interface across related classes. They
answer the question: 'What methods MUST every concrete subclass implement?' Without abstract
classes, a programmer could create a Shape subclass that forgets to implement area() — abstract
classes make this a runtime TypeError.
3.12 Exception Handling in OOP Context
Exception handling is critically important in OOP because real-world objects interact with external
systems (files, databases, networks, user input) where errors are inevitable. Python's exception
handling integrates naturally with OOP — exceptions themselves are objects, and programmers can
create custom exception classes by inheriting from built-in exception classes.
3.12.1 Review of try-except-else-finally
The try-except structure allows a program to attempt a potentially failing operation and respond
gracefully to errors without crashing.
Complete try-except structure
try:
# Risky operation
risky_code()
except TypeError as e:
# Handle TypeError
print('Type error:', e)
except ValueError as e:
# Handle ValueError
print('Value error:', e)
except Exception as e:
# Catch any other exception
print('General error:', e)
else:
# Runs ONLY if no exception occurred
print('Operation succeeded!')
finally:
# ALWAYS runs
print('Cleanup code here')
3.12.2 Custom Exception Classes
In OOP, exceptions are objects. Python's exception hierarchy starts with BaseException at the top. All
user-defined exceptions should inherit from Exception (or a more specific built-in exception). Custom
exceptions allow programs to define domain-specific error conditions with meaningful names.
Program 27: Custom exception classes
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
[Link] = balance
[Link] = amount
super().__init__(
f'Cannot withdraw {amount}. Balance is only {balance}.')
class NegativeAmountError(Exception):
def __init__(self, amount):
super().__init__(f'Amount {amount} cannot be negative.')
class BankAccount:
def __init__(self, owner, balance):
[Link] = owner
self.__balance = balance
def withdraw(self, amount):
if amount < 0:
raise NegativeAmountError(amount)
if amount > self.__balance:
raise InsufficientFundsError(self.__balance, amount)
self.__balance -= amount
return amount
def deposit(self, amount):
if amount < 0:
raise NegativeAmountError(amount)
self.__balance += amount
# Using the class with exception handling
acc = BankAccount('Alice', 1000)
try:
[Link](500)
[Link](200)
[Link](2000) # This will raise InsufficientFundsError
except InsufficientFundsError as e:
print('Error:', e)
except NegativeAmountError as e:
print('Error:', e)
finally:
print('Transaction attempt complete')
Output
Error: Cannot withdraw 2000. Balance is only 1300.
Transaction attempt complete
3.12.3 Exception Handling with Classes
Exception handling is especially important in methods that interact with external resources or perform
validations. Best practices for exception handling in classes:
11. Validate inputs in __init__ and raise exceptions for invalid values.
12. Handle exceptions as close to where they occur as possible.
13. Create custom exception classes for domain-specific errors.
14. Always release resources (close files, connections) in finally blocks.
15. Never use bare 'except:' clauses — always specify the exception type.
Program 28: Validation in constructor
class Student:
def __init__(self, name, age, marks):
if not name or not isinstance(name, str):
raise ValueError('Name must be a non-empty string')
if not 5 <= age <= 100:
raise ValueError(f'Invalid age: {age}')
if not 0 <= marks <= 100:
raise ValueError(f'Marks {marks} must be between 0 and 100')
[Link] = name
[Link] = age
[Link] = marks
try:
s1 = Student('Alice', 20, 95) # Valid
print(f'Created: {[Link]}, {[Link]}')
s2 = Student('Bob', 20, 110) # Invalid marks
except ValueError as e:
print('Validation Error:', e)
3.13 Comprehensive Worked Programs
Program 1: Complex Number Class with Operator Overloading
Code
class Complex:
def __init__(self, real, imag=0.0):
[Link] = real
[Link] = imag
def __str__(self):
if [Link] >= 0:
return f'({[Link]} + {[Link]}j)'
return f'({[Link]} - {abs([Link])}j)'
def __add__(self, other):
return Complex([Link] + [Link], [Link] + [Link])
def __sub__(self, other):
return Complex([Link] - [Link], [Link] - [Link])
def __mul__(self, other):
real = [Link] * [Link] - [Link] * [Link]
imag = [Link] * [Link] + [Link] * [Link]
return Complex(real, imag)
def __eq__(self, other):
return [Link] == [Link] and [Link] == [Link]
C1 = Complex(2, 1)
C2 = Complex(5, 6)
print('C1 =', C1)
print('C2 =', C2)
print('C1 + C2 =', C1 + C2)
print('C1 - C2 =', C1 - C2)
print('C1 * C2 =', C1 * C2)
print('C1 == C2:', C1 == C2)
Output
C1 = (2 + 1j)
C2 = (5 + 6j)
C1 + C2 = (7 + 7j)
C1 - C2 = (-3 - 5j)
C1 * C2 = (4 + 17j)
C1 == C2: False
Program 2: Inheritance Hierarchy — Vehicle, Car, ElectricCar
Code
class Vehicle:
def __init__(self, brand, model, year):
[Link] = brand
[Link] = model
[Link] = year
def display(self):
print(f'Brand: {[Link]} | Model: {[Link]} | Year:
{[Link]}')
def start(self):
print(f'{[Link]} {[Link]} engine started')
class Car(Vehicle):
def __init__(self, brand, model, year, doors):
super().__init__(brand, model, year)
[Link] = doors
def display(self):
super().display()
print(f'Doors: {[Link]}')
class ElectricCar(Car):
def __init__(self, brand, model, year, doors, battery_kWh):
super().__init__(brand, model, year, doors)
self.battery_kWh = battery_kWh
def display(self):
super().display()
print(f'Battery: {self.battery_kWh} kWh')
def start(self):
print(f'{[Link]} {[Link]} electric motor started silently')
v = Vehicle('Honda', 'Activa', 2022)
c = Car('Toyota', 'Corolla', 2023, 4)
e = ElectricCar('Tesla', 'Model 3', 2024, 4, 75)
vehicles = [v, c, e]
for vehicle in vehicles:
[Link]()
[Link]()
print()
Program 3: Abstract Class — Animal Sound System
Code
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name):
[Link] = name
@abstractmethod
def speak(self):
pass
@abstractmethod
def move(self):
pass
def breathe(self): # Concrete method
print(f'{[Link]} breathes air')
class Dog(Animal):
def speak(self): print(f'{[Link]} says: Woof!')
def move(self): print(f'{[Link]} runs on four legs')
class Bird(Animal):
def speak(self): print(f'{[Link]} says: Tweet!')
def move(self): print(f'{[Link]} flies with wings')
class Fish(Animal):
def speak(self): print(f'{[Link]} makes bubbles')
def move(self): print(f'{[Link]} swims with fins')
animals = [Dog('Rex'), Bird('Tweety'), Fish('Nemo')]
for animal in animals:
[Link]()
[Link]()
[Link]()
print()
Program 4: Student Management System (Complete OOP)
Code
class InvalidMarksError(Exception):
pass
class Student:
student_count = 0 # Class variable shared by all
def __init__(self, name, roll, subjects_marks):
for m in subjects_marks:
if not 0 <= m <= 100:
raise InvalidMarksError(f'Invalid marks: {m}')
[Link] = name
[Link] = roll
[Link] = subjects_marks
Student.student_count += 1
def total_marks(self):
return sum([Link])
def percentage(self):
return self.total_marks() / (len([Link]) * 100) * 100
def grade(self):
p = [Link]()
if p >= 90: return 'A+'
elif p >= 80: return 'A'
elif p >= 70: return 'B'
elif p >= 60: return 'C'
else: return 'F'
def __str__(self):
return (f'Roll: {[Link]} | Name: {[Link]} | '
f'Total: {self.total_marks()} | '
f'%: {[Link]():.1f} | Grade: {[Link]()}')
try:
students = [
Student('Alice', 101, [95, 88, 92, 90, 87]),
Student('Bob', 102, [70, 75, 68, 72, 80]),
Student('Carol', 103, [55, 60, 52, 58, 65])
]
print(f'Total students: {Student.student_count}')
print('-' * 60)
for s in students:
print(s)
except InvalidMarksError as e:
print('Error:', e)
3.14 Unit Summary
Topic Key Points
OOP Principles Encapsulation, Inheritance, Polymorphism, Abstraction — the four
pillars
Topic Key Points
Class Definition class ClassName: body — blueprint/template for objects; no object
created until instantiated
Object Creation obj = ClassName() — instantiation; creates object in memory
Attributes Variables inside class; class attributes (shared) vs instance attributes
(per object)
Methods Functions inside class; first param always 'self' which references
current object
dot operator [Link] and [Link]() — access class members from outside
self parameter References current object; equivalent to 'this' in Java/C++; mandatory
first param
__init__ Constructor Automatically called when object created; initializes instance attributes
__del__ Destructor Called when last reference to object deleted; used for cleanup
Encapsulation Public (name), Protected (_name), Private (__name — name
mangling)
Persistent Storage [Link]() to save, [Link]() to load Python objects to/from files
isinstance() isinstance(obj, Class) — check if object is instance of class at runtime
Inheritance class B(A): — B inherits all of A's attributes and methods
Single Inheritance One parent → one child: class B(A)
Multilevel Inheritance Chain: A → B → C; class B(A): class C(B):
Multiple Inheritance Two+ parents: class C(A, B)
super() Function super().__init__() — call parent constructor; avoids code duplication
Method Overriding Subclass defines method with same name as superclass; subclass
version runs
Polymorphism Same method call on different object types produces type-appropriate
behavior
Method Overloading Not directly supported; achieved via default args or *args
Operator Overloading Special dunder methods: __add__, __sub__, __mul__, __eq__,
__lt__, __str__, __len__
Abstract Classes from abc import ABC, abstractmethod; forces subclasses to implement
methods
Custom Exceptions class MyError(Exception): — inherit from Exception for domain errors
try-except-finally try: risky code; except: handle error; else: success; finally: always runs
3.15 Review Questions
A. Multiple Choice Questions
16. What is the relation between a class and an object? a) A class is an instance of an object
b) An object is an instance of a class c) An object is an attribute of a class d) None
[Answer: b]
17. Which method is automatically called when a new object is created? a) __del__ b)
__new__ c) __init__ d) __create__ [Answer: c]
18. What is the first parameter of every instance method in Python called? a) this b) cls c)
self d) obj [Answer: c]
19. The __del__ destructor method is invoked when: a) All references to the instance are
removed b) The first reference is created c) The object is initialized d) The class is defined
[Answer: a]
20. What type of inheritance does 'class C(A, B):' represent? a) Single b) Multilevel c)
Multiple d) Hierarchical [Answer: c]
21. Which special method is called when the + operator is used on an object? a) __plus__ b)
__add__ c) __sum__ d) __inc__ [Answer: b]
22. What does the following print? class A: pass; class B(A): pass; print(issubclass(B, A)) a)
False b) True c) Error d) None [Answer: b]
23. How do you make an attribute private in Python? a) private attribute b) #attribute c)
__attribute d) -attribute [Answer: c]
24. What does super().__init__() do in a subclass? a) Destroys the parent object b) Creates a
new parent object c) Calls the parent class constructor d) Overrides the parent class
[Answer: c]
25. Which module provides the ABC and abstractmethod for creating abstract classes? a)
abstract b) abc c) base d) interface [Answer: b]
B. True or False
26. All attributes and methods in Python are public by default. (True)
27. The __init__ method can return a value. (False — it implicitly returns None)
28. In Python, a private attribute with double underscores is completely inaccessible. (False —
name mangling renames it to _ClassName__attr but doesn't delete it)
29. Method overloading (multiple methods with same name, different params) is directly
supported in Python. (False)
30. Method overriding (subclass method with same name as superclass) is supported in Python.
(True)
31. super() can only be used in __init__ methods. (False — it can be used in any method)
32. An abstract class can be instantiated directly. (False — it raises TypeError)
33. The destructor __del__ is called exactly once per object. (True)
34. Every class in Python inherits from the object class. (True)
35. Custom exception classes must inherit from Exception. (True for best practice — technically
BaseException, but Exception is standard)
C. Short Answer Questions
36. Explain the four pillars of object-oriented programming with brief examples.
37. What is the purpose of the self parameter in Python? What happens if you forget to include it?
38. Explain the difference between a class attribute and an instance attribute. Give examples.
39. What is method overriding? How is it different from method overloading? Why doesn't Python
support method overloading directly?
40. Explain the role of super() in inheritance. Show how it avoids code duplication.
41. What is name mangling in Python? How does it implement private attribute protection?
42. What is the difference between is (identity) and == (equality) operators? Give an example
where they produce different results.
43. What is an abstract class? How does it enforce a contract on subclasses?
44. Explain operator overloading with the __str__ method. Why is implementing __str__ useful?
45. What is the purpose of the pickle module? How is it used for persistent object storage?
D. Programming Exercises
46. Create a class Circle with attributes radius. Implement methods for area() and
circumference(). Override __str__ to display the circle's details in a formatted way.
47. Write a program implementing a Shape hierarchy: Shape (abstract base) → Circle, Rectangle,
Triangle. Each subclass must implement area() and perimeter(). Demonstrate polymorphism
by creating a list of shapes and computing the total area.
48. Create a BankAccount class with private balance. Implement deposit(), withdraw(), and
get_balance() methods. Add a custom exception InsufficientFundsError. Test with try-except.
49. Create a class Vector2D representing a 2D vector. Overload +, -, *, == operators and __str__.
Test with: v1 = Vector2D(3, 4), v2 = Vector2D(1, 2), print(v1 + v2), print(v1 * 2).
50. Design a multilevel inheritance hierarchy: Person → Employee → Manager. Person has name
and age. Employee adds department and salary. Manager adds team_size and bonus. Use
super() in all constructors. Display complete info of a Manager object.
51. Create an abstract class Animal with abstract methods speak() and move(). Implement Dog,
Cat, and Bird subclasses. Create a list of mixed animals and call speak() on each
(demonstrating polymorphism).
52. Write a Student management system class that: validates marks (0-100) in __init__,
calculates percentage, assigns grade, overloads __str__ for formatted output, and uses a
class variable to count total students.
53. Implement operator overloading for a Matrix class: support + and * operators, __str__ for
formatted display, and __eq__ for equality comparison.
54. Demonstrate the destructor __del__ by creating a ResourceManager class that prints
messages on creation and destruction. Create multiple aliases and observe when __del__ is
called.
55. Write a program that saves and loads a list of Student objects using the pickle module. Verify
the data is preserved correctly after loading from disk.
E. Fill in the Blanks
56. A function defined inside a class is called a ______. (method)
57. The ______ method is automatically called when an object is created. (__init__)
58. The first parameter of every instance method is ______, which references the current object.
(self)
59. Private attributes in Python are prefixed with ______ underscores. (two / double)
60. The process of creating a new object from a class is called ______. (instantiation)
61. In Python, ______ inheritance means one class inherits from two or more base classes.
(multiple)
62. The ______ function is used to call the parent class constructor from a child class. (super())
63. Method ______ occurs when a subclass defines a method with the same name as the parent
class. (overriding)
64. An ______ class cannot be instantiated directly and forces subclasses to implement certain
methods. (abstract)
65. Python uses ______ module to serialize and deserialize Python objects for persistent storage.
(pickle)