Evaluate the fundamentals of OOP
Object Oriented Programming (OOP) is a programming paradigm that organises software around objects rather than functions .
What is OOP?
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects rather than functions.
Traditional (Procedural) Approach
Programs are built as a sequence of instructions acting on separate data:
Data A → Function 1 → Data B → Function 2 → Result
Data and functions are loosely coupled — functions operate on data without strong ownership.
OOP Approach
Programs are built around objects — self-contained units that bundle data (attributes) and behaviour (methods) together:
Object
┌─────────────────┐
│ Attributes │ ← Data (variables)
│ ──────────── │
│ Methods │ ← Behaviour (functions)
└─────────────────┘
Objects are created from a class, which acts as a blueprint.
Why Use OOP?
| Advantage | Explanation |
|---|---|
| Real-world modelling | Objects map naturally to real-world entities (e.g., a Student, BankAccount) |
| Data protection | Encapsulation hides internal data, preventing unintended access or modification |
| Code reuse | Inheritance allows new classes to reuse existing code without rewriting |
| Easier debugging | Each object is self-contained — errors are isolated to specific classes |
| Scalability | Large programs are easier to maintain by dividing responsibilities into objects |
The Three Core OOP Concepts
| Concept | Core idea | Key benefit |
|---|---|---|
| Encapsulation | Bundling data + methods inside a class; restricting access | Data protection |
| Inheritance | A class inherits attributes/methods from a parent class | Code reuse |
| Polymorphism | The same method name behaves differently depending on the object | Flexibility |
Classes and Objects
Class: Dog (blueprint)
├── attributes: name, breed, age
└── methods: bark(), eat(), sleep()
Objects: (instances of Dog)
├─ Buddy ─→ Dog("Buddy", "Labrador", 3)
├─ Max ─→ Dog("Max", "Beagle", 5)
└─ Luna ─→ Dog("Luna", "Husky", 2)
Defining a Class in Python
class Dog:
"""A class to represent a dog."""
# Class variable — shared by all instances
species = "Canis familiaris"
# Constructor — called when creating a new object
def __init__(self, name: str, breed: str, age: int):
# Instance variables — unique to each object
self.name = name
self.breed = breed
self.age = age
# Instance method — operates on the object
def bark(self) -> str:
return f"{self.name} says Woof!"
def __str__(self) -> str:
return f"{self.name} is a {self.age}-year-old {self.breed}."
Creating Objects
buddy = Dog("Buddy", "Labrador", 3)
luna = Dog("Luna", "Husky", 2)
print(buddy.bark()) # Buddy says Woof!
print(luna) # Luna is a 2-year-old Husky.
print(Dog.species) # Canis familiaris (shared by all Dog objects)
The self Keyword
self is a reference to the current instance of a class. It is used inside methods to:
- Access instance variables:
self.name - Call other methods:
self.bark() - Distinguish between instance variables and local variables with the same name
def __init__(self, name: str): # 'name' is a local parameter
self.name = name # 'self.name' is the instance variable
Instance Variables vs. Class Variables
| Instance Variable | Class Variable | |
|---|---|---|
| Scope | Belongs to a single object | Shared by all objects of the class |
| Defined in | Inside __init__ using self. | Directly inside the class body |
| Memory | Separate copy per object | One copy shared across all instances |
| Access | object.variable | ClassName.variable or object.variable |
| Example | self.name = "Buddy" | species = "Canis familiaris" |
class Dog:
species = "Canis familiaris" # Class variable — shared
def __init__(self, name: str):
self.name = name # Instance variable — unique
The __init__ Constructor
__init__ is a special method (also called a dunder method) that is automatically called when an object is created. It initializes the object’s attributes.
class BankAccount:
def __init__(self, owner: str, balance: float):
self.owner = owner
self.balance = balance
def deposit(self, amount: float):
self.balance += amount
def withdraw(self, amount: float):
if amount <= self.balance:
self.balance -= amount
else:
print("Insufficient funds.")
account = BankAccount("Alice", 1000.00) # __init__ is called automatically
account.deposit(500)
print(account.balance) # 1500.0
The Four Pillars of OOP
1. Encapsulation
Encapsulation is the bundling of data (attributes) and methods (behaviour) into a single unit (a class), and restricting direct access to an object’s internal data.
Why it matters:
- Prevents unauthorized modification of data
- Ensures an object maintains a valid internal state
- Allows the internal implementation to change without breaking code that uses the class
Access Modifiers in Python
Python uses conventions (not strict rules) to indicate visibility:
| Modifier | Convention | Meaning |
|---|---|---|
| Public | attribute | Accessible from anywhere |
| Protected | _attribute | Intended for internal use; a “soft warning” |
| Private | __attribute | Name mangling makes it harder (but not impossible) to access from outside |
class BankAccount:
def __init__(self, balance: float):
self.__balance = balance # Private — name becomes _BankAccount__balance
def deposit(self, amount: float):
if amount > 0:
self.__balance += amount
def get_balance(self) -> float:
return self.__balance # Controlled access via a method
account = BankAccount(1000)
# account.__balance → AttributeError: 'BankAccount' object has no attribute '__balance'
account.deposit(500)
print(account.get_balance()) # 1500.0 ✅ Controlled access
2. Inheritance
Inheritance allows a child class to reuse the attributes and methods of a parent class, without rewriting them.
Animal (Parent / Superclass)
│
├── eat() ← inherited by all children
└── breathe() ← inherited by all children
│
┌────┴────┐
│ │
Dog Cat (Child / Subclass)
bark() meow()
Single Inheritance
class Animal:
def __init__(self, name: str):
self.name = name
def eat(self):
print(f"{self.name} is eating.")
class Dog(Animal): # Dog inherits from Animal
def __init__(self, name: str):
super().__init__(name)
def bark(self):
print(f"{self.name} says Woof!")
buddy = Dog("Buddy")
buddy.eat() # Inherited from Animal → Buddy is eating.
buddy.bark() # Defined in Dog → Buddy says Woof!
Multiple Inheritance
Python supports a class inheriting from multiple parent classes:
class Flyable:
def fly(self):
print("Flying!")
class Swimmable:
def swim(self):
print("Swimming!")
class Duck(Animal, Flyable, Swimmable):
pass
duck = Duck("Donald")
duck.eat() # From Animal
duck.fly() # From Flyable
duck.swim() # From Swimmable
The super() Function
super() calls a method from the parent class, commonly used in __init__ to extend (not replace) the parent’s initialization:
class Dog(Animal):
def __init__(self, name: str, breed: str):
super().__init__(name) # Call parent's __init__
self.breed = breed # Add Dog-specific attribute
3. Polymorphism
Polymorphism (Greek: “many forms”) allows the same method name to behave differently depending on the object that calls it.
Method Overriding
A child class can override a method from its parent class:
class Animal:
def speak(self):
print("Some sound")
class Cat(Animal):
def speak(self): # Override parent's speak()
print("Meow!")
class Dog(Animal):
def speak(self): # Override parent's speak()
print("Woof!")
animals = [Animal("??"), Cat("Whiskers"), Dog("Buddy")]
for animal in animals:
animal.speak()
# Some sound
# Meow!
# Woof!
Polymorphism with Functions
def makeSpeak(animal: Animal):
animal.speak()
makeSpeak(Cat("Whiskers")) # Meow!
makeSpeak(Dog("Buddy")) # Woof!
The function makeSpeak works with any object that has a speak() method — regardless of the specific class.
4. Abstraction
Abstraction hides complex implementation details and exposes only the essential features of an object.
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract class
@abstractmethod
def area(self) -> float: # Abstract method — must be implemented by subclasses
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float: # Must implement abstract method
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
Four Pillars Summary
| Pillar | Purpose | Mechanism |
|---|---|---|
| Encapsulation | Bundle data + methods; protect internal state | Private attributes + public methods |
| Inheritance | Reuse code from parent classes | class Child(Parent) |
| Polymorphism | Same method name, different behaviour | Method overriding + function parameters |
| Abstraction | Hide complexity; expose essential interface | Abstract classes + abstract methods |
Benefits of OOP — Summary
| Benefit | Explanation |
|---|---|
| Modularity | Each class/object is self-contained — easy to develop and test independently |
| Reusability | Inheritance and composition allow existing classes to be reused and extended |
| Scalability | Well-structured OOP code scales better for large, complex applications |
| Maintainability | Changes to one class have minimal impact on others due to encapsulation |
| Real-world modelling | Objects map directly to real-world entities, making design intuitive |
OOP vs. Procedural Programming
| Procedural | OOP | |
|---|---|---|
| Organisation | Functions + global data | Objects (data + methods together) |
| Data access | Data is often global and exposed | Data is encapsulated within objects |
| Code reuse | Copy-paste or function calls | Inheritance |
| State management | Global variables or parameters | Object attributes |
| Best for | Simple scripts, data processing | Large, complex, real-world applications |
| Example | C, Fortran | Python, Java, C++ |
3## B3.1.2 Construct a design of classes, their methods and behavior
UML Class Diagrams
UML (Universal Modelling Language) provides a standardized way to visualize class structure before writing code. Each class is drawn as a three-compartment box:
┌─────────────────────────────────────┐
│ Class Name │ ← Class name (bold, centred)
├─────────────────────────────────────┤
│ – attributeName: Type │ ← Attributes (visibility: – private, + public, # protected)
│ – attributeName: Type │
├─────────────────────────────────────┤
│ + methodName(): ReturnType │ ← Methods (+ public, – private)
│ + methodName(param: Type): Return │
└─────────────────────────────────────┘
Visibility modifiers in UML:
| Symbol | Modifier | Meaning |
|---|---|---|
+ | Public | Accessible from any class |
– | Private | Accessible only within this class |
# | Protected | Accessible within this class and its subclasses |
Example — Person class:
┌─────────────────────────────────────────┐
│ Person │
├─────────────────────────────────────────┤
│ – name: String │
│ – dateOfBirth: String │
│ # address: String │
│ – age: int │
├─────────────────────────────────────────┤
│ + getName(): String │
│ + getAge(): int │
│ + setName(name: String): void │
└─────────────────────────────────────────┘
Designing a Class — Step by Step
When designing a class, work through these steps:
- Identify the class name — a noun describing the entity (e.g.,
Person,Student) - Identify attributes — what data does it store? (use visibility modifiers)
- Identify methods — what behaviours does it expose?
- Draw the UML diagram first — before writing any code
- Write the code from the diagram
Implementing a Class from UML in Python
Person Class
from datetime import date
class Person:
"""A class to represent a person."""
def __init__(self, name: str, date_of_birth: str):
self.__name = name # Private attribute
self.__date_of_birth = date_of_birth # Private attribute
def get_name(self) -> str: # + getName(): String
return self.__name
def get_age(self) -> int: # + getAge(): int
born = date.fromisoformat(self.__date_of_birth)
today = date.today()
age = today.year - born.year
if (today.month, today.day) < (born.month, born.day):
age -= 1
return age
Student Class (Inherits from Person)
class Student(Person):
"""A class to represent a student, inheriting from Person."""
def __init__(self, name: str, date_of_birth: str, student_id: str):
super().__init__(name, date_of_birth) # Call parent's __init__
self.__student_id = student_id
def get_student_id(self) -> str: # + getStudentID(): String
return self.__student_id
s = Student("Alice", "2006-03-15", "S001")
print(s.get_name()) # Alice (inherited from Person)
print(s.get_age()) # 20 (computed from Person)
print(s.get_student_id()) # S001 (Student-specific)
OOP Relationships
Classes are rarely isolated — they relate to each other. There are four main types of relationships:
| Relationship | Strength | Symbol | Description |
|---|---|---|---|
| Inheritance | Strongest | —▷ (hollow arrow) | “is-a” — a subclass is a type of parent |
| Composition | Strong | ——◆ (filled diamond) | “part-of” — parts cannot exist without the whole |
| Aggregation | Moderate | ——◇ (hollow diamond) | “has-a” — parts can exist independently |
| Association | Weakest | ——> | A uses B, but no ownership |
1. Inheritance (“is-a”)
A subclass is a specialised version of its parent class.
Person
▲
│
Student
▲
│
┌──────┴──────┐
│ │
PartTimeStudent ExchangeStudent
- UML symbol: hollow arrow
——▷pointing from child to parent - Programming:
class Child(Parent) - Example:
Student“is-a”Person
2. Composition (“part-of”)
The whole owns its parts — if the whole is destroyed, the parts are destroyed too.
Customer Address
┌───────────────┐ ┌───────────────┐
│ – name │ │ – address_id │
│ – email │◆─────│ – street │
│ │ │ – city │
└───────────────┘ └───────────────┘
(filled diamond = composition)
class Address:
def __init__(self, street: str, city: str):
self.__street = street
self.__city = city
def get_street(self) -> str:
return self.__street
class Customer:
def __init__(self, name: str, street: str, city: str):
self.__name = name
self.__address = Address(street, city) # Customer owns the Address object
def get_address(self) -> Address:
return self.__address
3. Aggregation (“has-a”)
A container holds references to parts, but the parts can exist independently of the container.
Team Player
┌───────────┐ ┌─────────────┐
│ – players │◇─────│ – name │
└───────────┘ └─────────────┘
(hollow diamond = aggregation)
class Player:
def __init__(self, name: str, jersey_number: int):
self.__name = name
self.__jersey_number = jersey_number
def get_name(self) -> str:
return self.__name
class Team:
def __init__(self, team_name: str):
self.__team_name = team_name
self.__players: list[Player] = []
def add_player(self, player: Player): # Player is passed in — created outside
self.__players.append(player)
def get_players(self) -> list[Player]:
return self.__players
4. Association
The weakest relationship — one class simply uses another, with no ownership.
Order Product
┌───────────┐ ┌──────────────┐
│ – date │<─────│ – name │
│ – items │ │ – price │
└───────────┘ └──────────────┘
class Product:
def __init__(self, name: str, price: float):
self.__name = name
self.__price = price
def get_price(self) -> float:
return self.__price
class Order:
def __init__(self, order_date: str):
self.__order_date = order_date
self.__items: list[Product] = []
def add_item(self, product: Product):
self.__items.append(product)
def get_total(self) -> float:
return sum(item.get_price() for item in self.__items)
Relationship Summary
| Inheritance | Composition | Aggregation | Association | |
|---|---|---|---|---|
| Analogy | “is-a” | “part-of” | “has-a” | “uses-a” |
| Ownership | Parent owns structure | Whole owns parts | Container holds parts | No ownership |
| Lifecycle | Child needs parent | Parts die with whole | Parts independent | Loose coupling |
| UML symbol | ——▷ (hollow arrow) | ——◆ (filled diamond) | ——◇ (hollow diamond) | ——> (arrow) |
| Example | Student is-a Person | Customer has Address | Team has Players | Order uses Product |
Best Practices for Class Design
| Practice | Why it matters |
|---|---|
| Single Responsibility Principle | Each class should have one clear purpose — avoid “god classes” |
| Start with UML | Sketching the class structure before coding prevents redesign later |
| Encapsulate attributes | Make attributes private; expose controlled access via getters/setters |
| Use meaningful names | Class names: nouns (Student, BankAccount). Method names: verbs (deposit, withdraw) |
| Minimise dependencies | Classes should be loosely coupled — avoid excessive cross-dependencies |
| Choose composition over inheritance when unsure | Inheritance creates tight coupling; composition is more flexible |
| Document behaviour, not implementation | Method comments should explain what it does, not how internally |