OOP Principles: Encapsulation Inheritance Polymorphism
OOP Principles: Encapsulation, Inheritance, and Polymorphism
Object-Oriented Programming (OOP) organises code around objects — self-contained units that bundle data (attributes) and behaviour (methods). The four pillars of OOP are encapsulation, inheritance, polymorphism, and abstraction.
Classes and Objects
A class is a blueprint; an object (instance) is a specific example built from that blueprint.
class Dog:
def __init__(self, name, breed):
self.__name = name # private attribute
self.__breed = breed # private attribute
def bark(self):
return f"{self.__name} says Woof!"
def get_name(self):
return self.__name
fido = Dog("Fido", "Labrador") # object/instance
Encapsulation
Encapsulation means bundling data and methods together, and hiding internal state from outside access. External code interacts only through the object's public interface (methods).
Why encapsulate?
- Data protection: Prevents invalid states (e.g., a negative age)
- Modularity: Changes to internal implementation don't break external code
- Maintainability: Clear boundaries between components
Access modifiers:
| Modifier | Python convention | Java/C# keyword | Visibility |
|---|---|---|---|
| Public | self.name | public | Accessible everywhere |
| Private | self.__name | private | Only within the class |
| Protected | self._name | protected | Within the class and subclasses |
Getters and setters (accessor and mutator methods) provide controlled access:
class BankAccount:
def __init__(self, balance):
self.__balance = balance # private
def get_balance(self): # getter
return self.__balance
def deposit(self, amount): # setter with validation
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit must be positive")
Inheritance
Inheritance allows a new class (subclass/child) to reuse the attributes and methods of an existing class (superclass/parent), then extend or modify them.
class Animal: # superclass
def __init__(self, name):
self._name = name
def speak(self):
return "..."
class Cat(Animal): # subclass inherits from Animal
def speak(self): # overrides parent method
return f"{self._name} says Meow!"
class Dog(Animal):
def speak(self):
return f"{self._name} says Woof!"
def fetch(self): # new method, only in Dog
return f"{self._name} fetches the ball"
Key concepts:
- The subclass inherits all public and protected attributes and methods
- The subclass can override methods to provide specialised behaviour
- The subclass can extend by adding new attributes and methods
- Use
super()to call the parent's constructor or methods
Benefits of inheritance:
- Code reuse: Common code written once in the superclass
- Hierarchy: Models real-world "is-a" relationships (a Cat IS AN Animal)
- Extensibility: New subclasses can be added without changing existing code
Polymorphism
Polymorphism ("many forms") means the same method name behaves differently depending on the object's type. There are two forms:
1. Method overriding (runtime polymorphism):
The subclass provides its own implementation of a method inherited from the superclass.
animals = [Cat("Whiskers"), Dog("Rex"), Cat("Luna")]
for animal in animals:
print(animal.speak()) # calls the correct version automatically
Output:
Whiskers says Meow!
Rex says Woof!
Luna says Meow!
The same .speak() call produces different output depending on the actual type — this is polymorphism.
2. Method overloading (compile-time polymorphism):
Multiple methods with the same name but different parameter lists (common in Java/C#, not natively supported in Python).
// Java example
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
Abstraction
Abstraction means hiding complex implementation details and showing only the essential features. In OOP, this is achieved through:
- Abstract classes: Cannot be instantiated; define a template for subclasses
- Abstract methods: Declared but not implemented in the abstract class; subclasses MUST implement them
- Interfaces: Define a contract of methods a class must implement (Java)
from abc import ABC, abstractmethod
class Shape(ABC): # abstract class
@abstractmethod
def area(self): # abstract method - no implementation
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape): # must implement ALL abstract methods
def __init__(self, radius):
self.__radius = radius
def area(self):
return 3.14159 * self.__radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.__radius
Composition vs Inheritance
Composition ("has-a") is an alternative to inheritance ("is-a"):
class Engine:
def start(self):
return "Engine running"
class Car:
def __init__(self):
self.engine = Engine() # Car HAS an Engine (composition)
def drive(self):
return self.engine.start() + " - driving"
Use inheritance when there is a genuine "is-a" relationship. Use composition when one object contains or uses another.
Design Principles
| Principle | Meaning |
|---|---|
| Single Responsibility | Each class should do one thing well |
| Open/Closed | Open for extension, closed for modification |
| Liskov Substitution | Subclass objects should work wherever parent objects are expected |
| DRY | Don't Repeat Yourself — use inheritance/composition to eliminate duplication |
Exam Tips
- Define each OOP term with a clear one-sentence definition then give a code example
- Encapsulation = data hiding + public interface. Don't just say "bundling data and methods"
- Inheritance models "is-a"; composition models "has-a" — know when each is appropriate
- Polymorphism means the same method call behaves differently for different types — always illustrate with a loop over a list of mixed objects
- In pseudocode questions, show constructors, method definitions, and
super()calls clearly - Abstract classes cannot be instantiated — this is a common exam trap