Unit 6 · 7 teaching hours · 18 min read
Object-Oriented Programming in Python
What the syllabus asks for 6
- Classes and objects
- Encapsulation
- Inheritance and polymorphism
- Special methods and operator overloading
- Dataclasses and composition
- Seeing the ideas: one mould, many objects
When a program grows beyond a few hundred lines, scattered variables and functions become hard to manage. Object-oriented programming (OOP) bundles data and the functions that work on it into objects.
You met OOP with Java in Semester II; here is how Python does it, with less ceremony and a few surprises.
- Classes, objects,
__init__, instance and class attributes, methods. - Encapsulation conventions, properties.
- Inheritance,
super(), polymorphism, abstract classes. - Special ("dunder") methods and operator overloading; dataclasses; composition.
6.1 Classes and objects
A class is a blueprint; an object (instance) is something built from it. __init__ is the initialiser, called when an object is created; self is the object being worked on (always the first parameter of an instance method).
class Student:
college = "Patan Multiple Campus" # class attribute (shared)
def __init__(self, name, roll):
self.name = name # instance attributes
self.roll = roll
self.marks = []
def add_mark(self, m):
self.marks.append(m)
def average(self):
return sum(self.marks) / len(self.marks) if self.marks else 0.0
s1, s2 = Student("Sita", 1), Student("Ram", 2)
for m in (80, 90, 70):
s1.add_mark(m)
print(s1.name, s1.average(), s2.average(), s1.college is s2.college)
Student.college = "Thapathali Campus"
print(s2.college, type(s1), isinstance(s1, Student))
Sita 80.0 0.0 True
Thapathali Campus <class '__main__.Student'> True
Instance attributes belong to one object; class attributes are shared by all. Python has no declarations: an attribute exists once assigned. Methods are ordinary functions stored in the class; s1.average() is shorthand for Student.average(s1).
class Counter:
count = 0
def __init__(self):
Counter.count += 1
@classmethod
def how_many(cls):
return cls.count
@staticmethod
def is_even(n):
return n % 2 == 0
a, b, c = Counter(), Counter(), Counter()
print(Counter.how_many(), Counter.is_even(7), c.count)
3 False 3
A class method receives the class (cls); a static method receives neither and is just a function in the class's namespace.
6.2 Encapsulation
Python has no private keyword; it relies on convention:
_name: "internal; please don't touch" (a single underscore).__name: name mangling: stored as_ClassName__nameto avoid accidental clashes in subclasses (not true privacy).
A property lets you keep a clean attribute-style interface while controlling access:
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
def deposit(self, amount):
self.balance += amount
acct = Account("Ram", 100)
acct.deposit(50)
print(acct.balance)
try:
acct.balance = -5
except ValueError as e:
print("rejected:", e)
print(sorted(k for k in vars(acct)))
150
rejected: balance cannot be negative
['_balance', 'owner']
6.3 Inheritance and polymorphism
A subclass inherits attributes and methods of its parent and can override them. super() calls the parent's version.
class Employee:
def __init__(self, name, base):
self.name, self.base = name, base
def pay(self):
return self.base
def __str__(self):
return f"{type(self).__name__}({self.name}, pay={self.pay()})"
class Manager(Employee):
def __init__(self, name, base, bonus):
super().__init__(name, base)
self.bonus = bonus
def pay(self):
return super().pay() + self.bonus
class Intern(Employee):
def pay(self):
return self.base * 0.5
staff = [Employee("Hari", 40000), Manager("Gita", 60000, 15000), Intern("Bikash", 20000)]
for p in staff:
print(p)
print(sum(p.pay() for p in staff), isinstance(staff[1], Employee), issubclass(Intern, Employee))
print(Manager.__mro__[1].__name__, [c.__name__ for c in Manager.__mro__])
Employee(Hari, pay=40000)
Manager(Gita, pay=75000)
Intern(Bikash, pay=10000.0)
125000.0 True True
Employee ['Manager', 'Employee', 'object']
Polymorphism: the loop calls p.pay() and each object responds according to its own class, with no if on the type.
Python is duck-typed: "if it walks like a duck and quacks like a duck, it is a duck"; any object with a pay() method would work.
Multiple inheritance is allowed (class C(A, B)); the method resolution order (MRO) decides which parent is searched first (C.__mro__).
Abstract classes
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self): ...
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r ** 2
class Rect(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
print([round(s.area(), 2) for s in (Circle(1), Rect(3, 4))])
try:
Shape()
except TypeError as e:
print("TypeError:", e)
[3.14, 12]
TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
An abstract class cannot be instantiated until subclasses implement every abstract method.
6.4 Special methods and operator overloading
Methods with double underscores let your objects work with built-in syntax.
| Method | Called by |
|---|---|
__init__, __del__ |
creation, destruction |
__str__, __repr__ |
str(x) / print, repr(x) (debugging) |
__len__, __getitem__, __iter__, __contains__ |
len, x[i], for, in |
__add__, __sub__, __mul__, __eq__, __lt__ |
+ - * == < |
__call__, __enter__/__exit__ |
obj(), with obj: |
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k):
return Vector(self.x * k, self.y * k)
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __len__(self):
return 2
v, w = Vector(3, 4), Vector(1, 2)
print(v + w, v * 3, abs(v), v == Vector(3, 4), len(v))
Vector(4, 6) Vector(9, 12) 5.0 True 2
6.5 Dataclasses and composition
Boilerplate classes that just hold data are quicker with @dataclass, which writes __init__, __repr__ and __eq__ for you.
from dataclasses import dataclass, field
@dataclass(order=True)
class Course:
code: str
credits: int = 3
topics: list = field(default_factory=list, compare=False)
c1, c2 = Course("BCA 251", 3), Course("BCA 256", 2)
c1.topics.append("deadlocks")
print(c1, c1 == Course("BCA 251", 3), sorted([c1, c2])[0].code)
Course(code='BCA 251', credits=3, topics=['deadlocks']) True BCA 251
Composition ("has-a") builds objects from other objects; it is often a better choice than inheritance ("is-a") when the relationship is not truly a specialisation. A Library has a list of Book objects; a Car has an Engine.
class Book:
def __init__(self, title, copies):
self.title, self.copies = title, copies
class Library:
def __init__(self):
self.books = {}
def add(self, title, copies=1):
book = self.books.setdefault(title, Book(title, 0))
book.copies += copies
def lend(self, title):
book = self.books.get(title)
if not book or book.copies == 0:
return False
book.copies -= 1
return True
lib = Library()
lib.add("SICP", 2)
print([lib.lend("SICP") for _ in range(3)], lib.lend("Missing"), lib.books["SICP"].copies)
[True, True, False] False 0
6.6 Seeing the ideas: one mould, many objects
Object-oriented programming asks you to describe a kind of thing once and then make as many of them as you need. The photograph shows the oldest picture of that idea, a metal cutter that stamps out any number of identical cookies.
The figures then show a class and its subclass, the order in which Python searches parent classes, and the special methods that make + and print work on your own objects.

Inheritance is easiest to picture with vehicles. A general Vehicle knows how to compute a fare per kilometre; a city bus and a Safa tempo are special kinds of Vehicle that reuse what they can and override what differs.
The tariffs below are invented for the exercise and are not the real fares of any operator.


6.6.1 Eight advantages of object-oriented programming
- Models real things naturally. A
Student,AccountorBookin the program matches the thing it stands for, with its data and actions in one place. A college system can use one class for a student and another for a course. This makes the design easier to explain to a client. - Encapsulation protects data. Attributes can be kept internal (a leading underscore) and changed only through methods that check the values. A
withdraw()method can refuse to make a balance negative. Without this, any line of the program could damage the data. - Reuse through inheritance. A
SavingsAccountgets everything ofAccountfor free and adds only what is new (Fig. 6.1). The shared code is written and tested once. The weakness is that a deep chain of parent classes becomes hard to follow, so keep hierarchies shallow. - Polymorphism gives flexible code. The same call, such as
shape.area(), works on a circle, a square or a triangle, and each object runs its own version. A loop can handle all of them withoutiftests on the type. New shapes can be added without changing the loop. - Easier maintenance. A change inside one class does not break the others as long as its public methods stay the same. A bug in fee calculation is searched for in the
Feeclass only. This saves time in large programs that live for years. - Many objects, one description. The class is written once and Plate 6.1 shows how many objects can be made from it, each with its own values. A school can make five hundred
Studentobjects from one definition. This keeps the program short and uniform. - Natural for teams and libraries. Each member can build and test a class, and the group joins them through their public methods. Libraries such as Pandas and Django expose classes (
DataFrame,Model) that you extend. Classes with clear names are also easier for a new member to read. - Operators and built-ins for your own types. With special methods (Fig. 6.3) your objects can use
+,==,len()andprint(), soa + bfor two vectors or two money amounts reads like mathematics. This makes the code short and clear. Use it with care: an operator that does something surprising makes the program harder to read.
Formula and fact sheet
class C(Parent):withdef __init__(self, …);selfis explicit.- Class attribute shared; instance attribute per object;
@classmethodgetscls;@staticmethodgets nothing. _xconvention,__xmangled;@propertyfor controlled attributes.super().method(); polymorphism through overriding; duck typing; MRO.__str__,__repr__,__eq__,__add__,__len__…;@dataclass.
Chapter summary
- Classes bundle data and behaviour; objects are instances;
selfand__init__are explicit. - Encapsulation in Python is by convention and properties.
- Inheritance specialises a class, polymorphism lets code treat different objects uniformly, and abstract classes force a common interface.
- Dunder methods integrate your classes with the language; dataclasses and composition keep designs small.
Practice
- Define a class
Rectanglewitharea()andperimeter()and test it. - Explain the difference between class and instance attributes with a program.
- Why does the
@propertydecorator exist? Show a validated setter. - Create
AnimalandDog,Catsubclasses that overridespeak(), and call them polymorphically. - What is an abstract class and when do you need one?
- Implement
__add__and__str__for aMoneyclass. - Compare inheritance and composition with an example of each.
Answers and worked solutions
1.
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
def perimeter(self):
return 2 * (self.w + self.h)
r = Rectangle(3, 5)
print(r.area(), r.perimeter())
15 16
2.
class A:
shared = []
def __init__(self, tag):
self.tag = tag
a, b = A("x"), A("y")
a.shared.append(1)
print(b.shared, a.tag, b.tag)
[1] x y
shared is one list for all instances; tag is separate for each.
3. A property keeps attribute syntax while running code (validation, computed values); the Section 6.2 Account shows a setter that rejects negative balances.
4.
class Animal:
def speak(self): return "..."
class Dog(Animal):
def speak(self): return "Woof"
class Cat(Animal):
def speak(self): return "Meow"
print([a.speak() for a in (Dog(), Cat(), Animal())])
['Woof', 'Meow', '...']
5. A class that defines methods subclasses must implement (marked @abstractmethod) and cannot be instantiated itself; use it to enforce a common interface, such as Shape.area().
6.
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __str__(self):
return f"NPR {self.amount:,.2f}"
print(Money(1500) + Money(250.5))
NPR 1,750.50
7. Inheritance models "is-a": Manager is an Employee. Composition models "has-a": a Library has Books, a Car has an Engine. Prefer composition when the relationship is not a true specialisation, since it is more flexible and avoids deep hierarchies.
Model answers (5 and 10 marks)
Q1. Explain class, object, constructor and self in Python with a program. (5 marks)
A class is a blueprint that defines the attributes (data) and methods (functions) of a kind of object. An object (instance) is one thing made from the class; it has its own attribute values (Plate 6.1). Objects are created by calling the class, Student("Ram", 72).
The constructor is the special method __init__(self, ...), which Python calls automatically when an object is created; it sets the starting values.
The parameter self is the object on which the method is running; Python passes it automatically, so s.show() means Student.show(s).
Attributes made with self.name = name belong to the object (instance attributes), while a variable written in the class body, such as school = "TU", is shared by all objects (class attribute).
class Student:
school = "TU"
count = 0
def __init__(self, name, marks):
self.name = name
self.marks = marks
Student.count += 1
def grade(self):
return "Pass" if self.marks >= 40 else "Fail"
def __str__(self):
return f"{self.name} ({self.marks}) {self.grade()}"
s1, s2 = Student("Ram", 72), Student("Hari", 35)
print(s1)
print(s2, "|", Student.count, s1.school is s2.school)
Ram (72) Pass
Hari (35) Fail | 2 True
Q2. Explain encapsulation and access control in Python. How does a property help? (5 marks)
Encapsulation means keeping data and the code that works on it together, and hiding the internal detail so that outside code can use the object only through its public methods. Python has no strict private keyword and relies on naming conventions.
- Public
name: free to use. - Protected
_name: a single underscore means "internal, do not use from outside". - Private
__name: a double underscore triggers name mangling; the attribute is stored as_Class__name, which prevents accidental clashes in subclasses, but it can still be reached by that long name.
A property lets you keep the simple syntax acc.balance while running code, for example validation, on every read or write. @property defines the getter and @balance.setter defines the setter.
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = 0
self.balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance cannot be negative")
self.__balance = value
a = Account("Sita", 500)
a.balance += 250
print(a.balance, sorted(k for k in vars(a)))
try:
a.balance = -1
except ValueError as e:
print("ValueError:", e)
750 ['_Account__balance', 'owner']
ValueError: balance cannot be negative
The output shows the mangled name _Account__balance, proving that the data is hidden by convention and not locked.
Q3. What is inheritance? Explain its types, method overriding, super() and polymorphism with a program, and compare the types of inheritance. (10 marks)
Introduction. Inheritance lets a new class (the subclass or child) take over the attributes and methods of an existing class (the superclass or parent) and add or change some of them.
It models an "is a" relationship, as SavingsAccount is an Account (Fig. 6.1), and it gives reuse of code. Polymorphism ("many forms") means that one call can behave differently depending on the object that receives it.
Syntax. class Child(Parent):.
A child can override a method by defining one with the same name. super() gives access to the next class in the method resolution order, so super().__init__(...) runs the parent constructor and super().withdraw(x) reuses the parent's method.
Types of inheritance.
- Single: one parent, one child.
- Multilevel: a chain, grandparent to parent to child.
- Hierarchical: one parent with several children.
- Multiple: a child with two or more parents; Python searches them in the MRO (Fig. 6.2), which you can print with
Class.__mro__. - Hybrid: a combination, such as the diamond.
Polymorphism in Python works through overriding and duck typing: any object that has the needed method can be used, whatever its class.
class Shape:
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return round(3.14159 * self.r ** 2, 2)
class Square(Rectangle):
def __init__(self, s):
super().__init__(s, s)
shapes = [Rectangle(3, 4), Circle(2), Square(5)]
print([s.area() for s in shapes])
print(isinstance(shapes[2], Rectangle), issubclass(Square, Shape))
[12, 12.57, 25]
True True
Comparison.
| Type | Parents | Example | Main risk |
|---|---|---|---|
| Single | 1 | Square(Rectangle) |
none |
| Multilevel | chain | C(B), B(A) |
long chains are hard to follow |
| Hierarchical | 1 parent, many children | Circle, Rectangle from Shape |
parent change affects all |
| Multiple | 2 or more | D(B, C) |
ambiguity, solved by the MRO |
| Hybrid | mixed | the diamond | complexity |
Advantages: reuse, a natural hierarchy, and polymorphic code; disadvantages: tight coupling between parent and child, and deep hierarchies. Prefer composition ("has a") when "is a" does not fit.
Conclusion. Inheritance and polymorphism let one loop work on many kinds of objects; use them with shallow hierarchies and a clear "is a" relationship.