Skip to content

Object-Oriented Programming

A class is a blueprint (template) that defines the structure and behaviour of objects. An object is an instance of a class — a concrete entity with specific values for the attributes Defined by the class.

class BankAccount:
def __init__(self, account_number, owner, balance=0.0):
self._account_number = account_number
self._owner = owner
self._balance = balance
def deposit(self, amount):
if amount > 0:
self._balance += amount
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
def get_balance(self):
return self._balance
ConceptClassInstance (Object)
NatureTemplate / BlueprintConcrete realisation
NumberOne class definitionMany objects
CreationDefined by programmerCreated at runtime
MemoryOne copy of methodsOwn copy of instance vars

Encapsulation is the bundling of data (attributes) and methods that operate on that data within A class, and restricting direct access to internal state.

ModifierMeaningPython convention
PublicAccessible from anywherename
ProtectedAccessible within class and subclasses_name (convention)
PrivateAccessible only within the class__name (name mangling)
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def get_name(self):
return self.__name
def set_age(self, age):
if age >= 0:
self.__age = age

Benefits of encapsulation:

  1. Data hiding: Prevents unauthorised access
  2. Validation: Input validation through setters
  3. Flexibility: Internal implementation can change without affecting users
  4. Maintainability: Reduces coupling between components

Inheritance allows a class (subclass/child) to inherit attributes and methods from another class (superclass/parent), enabling code reuse and establishing an “is-a” relationship.

class Shape:
def __init__(self, colour):
self._colour = colour
def area(self):
return 0
def __str__(self):
return f"{self.__class__.__name__} (colour: {self._colour})"
class Rectangle(Shape):
def __init__(self, colour, width, height):
super().__init__(colour)
self._width = width
self._height = height
def area(self):
return self._width * self._height
class Circle(Shape):
def __init__(self, colour, radius):
super().__init__(colour)
self._radius = radius
def area(self):
return 3.14159 * self._radius ** 2
TypeDescriptionPython support
SingleOne child, one parentYes
MultipleOne child, multiple parentsYes
MultilevelChain: A → B → CYes
HierarchicalOne parent, multiple childrenYes
HybridCombination of the aboveYes

A subclass can override a method inherited from the superclass by defining a method with the Same name.

class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"

super() calls the parent class”s method, enabling extension (not replacement) of inherited Behaviour.


Polymorphism (Greek: “many forms”) allows objects of different classes to be treated through a Common interface, with the specific behaviour determined at runtime.

Achieved through method overloading (multiple methods with the same name but different Parameters). Python does not support method overloading directly, but can simulate it with default Arguments or type checking.

Achieved through method overriding and duck typing. The actual method called depends on the Object’s type at runtime.

def make_speak(animal):
print(animal.speak())
animals = [Dog(), Cat(), Animal()]
for animal in animals:
make_speak(animal)

Output:

Woof
Meow
Some sound

Theorem (Liskov Substitution Principle). If SS is a subtype of TTThen objects of type TT May be replaced with objects of type SS without altering any of the desirable properties of the Program.

This means: wherever a superclass object is expected, a subclass object should work correctly.


An abstract class cannot be instantiated and may contain abstract methods (methods without Implementation that must be implemented by subclasses).

from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def start_engine(self):
pass
@abstractmethod
def stop_engine(self):
pass
def drive(self):
self.start_engine()
print("Driving...")
self.stop_engine()

An interface is a contract specifying methods a class must implement, without providing any Implementation. In Python, interfaces are simulated using abstract classes with only Abstract methods.


6. Association, Aggregation, and Composition

Section titled “6. Association, Aggregation, and Composition”

A general relationship between two classes. Objects can exist independently.

class Doctor:
def __init__(self, name):
self.name = name
self.patients = []

A whole-part relationship where parts can exist independently of the whole.

class Department:
def __init__(self, name):
self.name = name
self.teachers = []
class Teacher:
def __init__(self, name):
self.name = name

A whole-part relationship where parts cannot exist without the whole.

class House:
def __init__(self, address):
self.address = address
self.rooms = [Room("living"), Room("bedroom")]
class Room:
def __init__(self, purpose):
self.purpose = purpose

When a House is destroyed, its Room objects are also destroyed.

RelationshipIndependenceLifecycleExample
AssociationIndependentIndependentDoctor-Patient
AggregationIndependentIndependentDepartment-Teacher
CompositionDependentPart dies with wholeHouse-Room

PrincipleNameDescription
SSingle ResponsibilityA class should have one reason to change
OOpen/ClosedOpen for extension, closed for modification
LLiskov SubstitutionSubtypes must be substitutable for their base types
IInterface SegregationClients shouldn’t depend on methods they don’t use
DDependency InversionDepend on abstractions, not concretions
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
pass
class EmailService:
def send_welcome(self, user):
pass

Each class has a single responsibility.


Problem 1. Design a class hierarchy for different types of employees in a company: Manager, Developer, and Intern. Include a common method calculate_salary() with different implementations.

Answer
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name, base_salary):
self.name = name
self.base_salary = base_salary
@abstractmethod
def calculate_salary(self):
pass
class Manager(Employee):
def __init__(self, name, base_salary, bonus):
super().__init__(name, base_salary)
self.bonus = bonus
def calculate_salary(self):
return self.base_salary + self.bonus
class Developer(Employee):
def __init__(self, name, base_salary, overtime_hours):
super().__init__(name, base_salary)
self.overtime_hours = overtime_hours
def calculate_salary(self):
return self.base_salary + self.overtime_hours * 50
class Intern(Employee):
def calculate_salary(self):
return self.base_salary

Problem 2. Explain the difference between a class variable and an instance variable. Give an Example.

Answer

A class variable is shared by all instances of the class (defined at the class level). An instance variable is unique to each instance (defined in __init__).

class Dog:
species = "Canis familiaris" # Class variable (shared)
def __init__(self, name):
self.name = name # Instance variable (unique)

Dog.species is the same for all dogs. dog1.name and dog2.name are different.

Problem 3. Explain how polymorphism is demonstrated in the following code:

shapes = [Rectangle("red", 3, 4), Circle("blue", 5)]
for shape in shapes:
print(shape.area())
Answer

The loop iterates over a list of Shape objects (actually Rectangle and Circle instances). When shape.area() is called, Python determines at runtime which area() method to invoke based on The actual type of the object:

  • For the Rectangle: calls Rectangle.area()3×4=123 \times 4 = 12
  • For the Circle: calls Circle.area()π×2578.54\pi \times 25 \approx 78.54

The same interface (area()) produces different behaviour for different types — this is run-time Polymorphism (also called dynamic dispatch).

Problem 4. A student writes a Square class that inherits from Rectangle. The Square Constructor takes only a side parameter. Explain why this might violate the Liskov Substitution Principle.

Answer
class Square(Rectangle):
def __init__(self, colour, side):
super().__init__(colour, side, side)

The LSP violation occurs if Rectangle allows independent setting of width and height:

r = Square("red", 5)
r.set_width(10) # If Rectangle has this, width = 10, height = 5
## Now r is no longer a valid square!

A Square used as a Rectangle can be put into an invalid state. This means Square is not a Proper subtype of Rectangle if Rectangle allows mutation of width and height independently.

Solution: Use composition instead (Square has-a Rectangle), or make Rectangle immutable, or use An interface-based approach.

Problem 5. Explain the difference between aggregation and composition with examples.

Answer

Aggregation (weak “has-a”): The part can exist independently of the whole. Example: A Department has Teacher objects. If the department is dissolved, the teachers still exist and can Join another department.

Composition (strong “has-a”): The part cannot exist independently of the whole. Example: A Car Has Engine and Wheel objects. If the car is destroyed, its specific engine and wheels are also Destroyed (they don’t make sense independently in this context).

In code: Aggregation passes in existing objects. Composition creates objects internally.

Problem 6. Implement an abstract class DataStructure with abstract methods insert``delete And search. Then implement it as a Stack.

Answer
from abc import ABC, abstractmethod
class DataStructure(ABC):
@abstractmethod
def insert(self, value):
pass
@abstractmethod
def delete(self):
pass
@abstractmethod
def search(self, value):
pass
class Stack(DataStructure):
def __init__(self):
self._data = []
def insert(self, value):
self._data.append(value)
def delete(self):
if self._data:
return self._data.pop()
raise Exception("Stack underflow")
def search(self, value):
for i in range(len(self._data) - 1, -1, -1):
if self._data[i] == value:
return i
return -1

Problem 7. Explain the Open/Closed Principle and give an example of a design that violates it, Then fix it.

Answer

Violation:

class AreaCalculator:
def area(self, shape):
if shape.type == "rectangle":
return shape.width * shape.height
elif shape.type == "circle":
return 3.14159 * shape.radius ** 2
# Adding a new shape requires modifying this class!

Fix (open for extension, closed for modification):

class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def area(self):
return self.width * self.height
class Circle(Shape):
def area(self):
return 3.14159 * self.radius ** 2
class AreaCalculator:
def total_area(self, shapes):
return sum(s.area() for s in shapes)

Adding a new shape requires only adding a new class — no modification to existing code.

Problem 8. What is the output of the following code? Explain the method resolution order.

class A:
def greet(self):
return "A"
class B(A):
def greet(self):
return "B"
class C(A):
def greet(self):
return "C"
class D(B, C):
pass
print(D().greet())
print(D.__mro__)
Answer

Output:

B
(<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

Python uses C3 Linearization (MRO — Method Resolution Order) to determine the order in which Base classes are searched for methods. For D(B, C):

  1. D itself → no greet
  2. B → has greetReturns “B”

The MRO is D → B → C → A → object. Since B has greetThe search stops there.

Problem 9. Explain why multiple inheritance can lead to the “diamond problem” and how Python Resolves it.

Answer

The diamond problem occurs when a class inherits from two classes that both inherit from the Same base class:

A
/ \
B C
\ /
D

If both B and C override a method from AWhich version does D inherit?

Python resolves this using C3 Linearization, which produces a deterministic, monotonically Increasing order. For D(B, C):

  • MRO: D, B, C, A, object
  • B’s version is preferred over C’s

If D calls super().__init__()Python follows the MRO, ensuring each class’s __init__ is Called exactly once.

Languages like C++ resolve this differently (requiring explicit disambiguation).

Problem 10. Design a library system with classes for Book``MemberAnd Library. Use Encapsulation appropriately. Include methods for borrowing and returning books.

Answer
class Book:
def __init__(self, isbn, title, author):
self.__isbn = isbn
self.__title = title
self.__author = author
self.__available = True
def is_available(self):
return self.__available
def borrow(self):
if self.__available:
self.__available = False
return True
return False
def return_book(self):
self.__available = True
class Member:
def __init__(self, member_id, name):
self.__member_id = member_id
self.__name = name
self.__borrowed_books = []
def borrow_book(self, library, isbn):
book = library.find_book(isbn)
if book and book.is_available():
book.borrow()
self.__borrowed_books.append(book)
return True
return False
def return_book(self, library, isbn):
for i, book in enumerate(self.__borrowed_books):
if book._Book__isbn == isbn:
book.return_book()
self.__borrowed_books.pop(i)
return True
return False
def get_borrowed_count(self):
return len(self.__borrowed_books)
class Library:
def __init__(self):
self.__books = {}
self.__members = {}
def add_book(self, book):
self.__books[book._Book__isbn] = book
def register_member(self, member):
self.__members[member._Member__member_id] = member
def find_book(self, isbn):
return self.__books.get(isbn)

For revision on programming fundamentals, see Programming Constructs.

  1. Confusing authentication (who you are) with authorisation (what you can do) in security contexts.

  2. Forgetting that O(nlogn)O(n \log n) average-case for quicksort becomes O(n2)O(n^2) worst-case on already sorted input.

  3. Neglecting to normalise database designs, leading to data redundancy and update anomalies.

  4. Mixing up Big O, Big Ω\Omega, and Big Θ\Theta notation. Big O is an upper bound, not necessarily tight.

This topic explores fundamental concepts that shape our understanding of the world.

The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.