Skip to content

Object-Oriented Programming

1. Classes and Objects

Definition

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.

Python Implementation

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

Class vs Instance

ConceptClassInstance (Object)
NatureTemplate / BlueprintConcrete realisation
NumberOne class definitionMany objects
CreationDefined by programmerCreated at runtime
MemoryOne copy of methodsOwn copy of instance vars

2. Encapsulation

Definition

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

Access Modifiers

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

:::tip Exam tip In exams, always justify why encapsulation is important. Focus on data integrity (preventing invalid states) and implementation flexibility. :::


3. Inheritance

Definition

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

Types of Inheritance

TypeDescriptionPython support
SingleOne child, one parentYes
MultipleOne child, multiple parentsYes
MultilevelChain: A → B → CYes
HierarchicalOne parent, multiple childrenYes
HybridCombination of the aboveYes

Method Overriding

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"

The super() Function

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


4. Polymorphism

Definition

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

Types of Polymorphism

Compile-Time (Static) Polymorphism

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.

Run-Time (Dynamic) Polymorphism

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.

:::info Board-specific AQA requires understanding of classes, objects, inheritance, Polymorphism, encapsulation; uses pseudocode class definitions. CIE (9618) covers OOP Principles; may require implementation in a specific language (Python/Pascal). OCR (A) requires Class definitions with attributes and methods; constructor/destructor understanding. Edexcel Covers OOP with pseudocode; emphasises encapsulation and data hiding. :::


5. Abstract Classes and Interfaces

Abstract Classes

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()

Interfaces

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

Association

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

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

Aggregation (“has-a”, weak)

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

Composition (“has-a”, strong)

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

7. SOLID Principles

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

Example: Single Responsibility

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 Set

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.

Common Pitfalls

  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.

Summary

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

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