🕯️ Magic Note
These four pillars—Encapsulation, Inheritance, Polymorphism, and Abstraction—are often called the “four pillars of OOP.” They were first articulated in the 1970s with the Simula language and later popularized by C++ and Java. Python implements all of them, but with its own unique flavor.
Python
class BankAccount:
def __init__(self, owner, initial_balance):
self.owner = owner
self._balance = initial_balance # Protected (convention)
self.__pin = 1234 # Private (name mangling)
def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount, pin):
if pin != self.__pin:
raise ValueError(“Invalid PIN”)
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
def get_balance(self):
return self._balance # Controlled access
account = BankAccount(“Ali”, 1000)
print(account.get_balance()) # 1000 (controlled)
# print(account._balance) # Works but violates convention (don’t do it)
# print(account.__pin) # AttributeError (name mangling hides it)
print(account._BankAccount__pin) # 1234 (accessible but don’t do it)
🕯️ Magic Note
Name mangling (double underscore) changes the attribute name to _ClassName__attribute. This is not true privacy—it is just a way to avoid accidental name conflicts in subclasses. The Python philosophy is “we are all consenting adults.” Encapsulation is a convention, not a wall.
| Convention | How to Write | Meaning |
|---|---|---|
| Public | self.name | Accessible from anywhere (default) |
| Protected | self._name | Internal use. Can be accessed but don’t. |
| Private | self.__name | Name mangling. Stronger protection. |
Python
class Example:
def __init__(self):
self.public = “Anyone can see me”
self._protected = “Please don’t touch me directly”
self.__private = “You can’t easily access me”
def get_private(self):
return self.__private # Provide controlled access
ex = Example()
print(ex.public) # Works
print(ex._protected) # Works but is discouraged
print(ex.get_private()) # Works (proper way)
# print(ex.__private) # AttributeError
Python
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError(“Subclass must implement this method”)
def move(self):
print(f”{self.name} is moving”)
# Dog inherits from Animal
class Dog(Animal):
def speak(self):
return “Woof!”
def fetch(self):
print(f”{self.name} is fetching the ball”)
# Cat inherits from Animal
class Cat(Animal):
def speak(self):
return “Meow!”
def climb(self):
print(f”{self.name} is climbing a tree”)
buddy = Dog(“Buddy”)
whiskers = Cat(“Whiskers”)
print(buddy.speak()) # Woof!
print(whiskers.speak()) # Meow!
buddy.move() # Buddy is moving (inherited)
whiskers.move() # Whiskers is moving (inherited)
buddy.fetch() # Buddy is fetching the ball (Dog-specific)
whiskers.climb() # Whiskers is climbing a tree (Cat-specific)
Python
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_info(self):
return f”Name: {self.name}, Salary: {self.salary}”
def work(self):
return f”{self.name} is working”
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary) # Call parent constructor
self.team_size = team_size
def get_info(self):
# Extend parent method
parent_info = super().get_info()
return f”{parent_info}, Team Size: {self.team_size}”
def work(self):
# Override with new behavior
return f”{self.name} is managing a team of {self.team_size} people”
emp = Employee(“Ali”, 50000)
mgr = Manager(“Sara”, 80000, 5)
print(emp.get_info()) # Name: Ali, Salary: 50000
print(mgr.get_info()) # Name: Sara, Salary: 80000, Team Size: 5
print(emp.work()) # Ali is working
print(mgr.work()) # Sara is managing a team of 5 people
🕯️ Magic Note
The super() function is even more powerful in multiple inheritance. It follows the Method Resolution Order (MRO) to call the next class in the inheritance chain, not necessarily the immediate parent.
Python
class Flyer:
def fly(self):
return “Flying through the air!”
def move(self):
return “Flying”
class Swimmer:
def swim(self):
return “Swimming through water!”
def move(self):
return “Swimming”
# Duck inherits from both Flyer and Swimmer
class Duck(Flyer, Swimmer):
def quack(self):
return “Quack!”
donald = Duck()
print(donald.fly()) # Flying through the air!
print(donald.swim()) # Swimming through water!
print(donald.quack()) # Quack!
print(donald.move()) # Flying (from Flyer – first parent)
# Check Method Resolution Order
print(Duck.__mro__) # (<class ‘Duck’>, <class ‘Flyer’>, <class ‘Swimmer’>, <class ‘object’>)
Python
class Circle:
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
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
def perimeter(self):
return 4 * self.side
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# Polymorphic function
def print_shape_info(shape):
print(f”Area: {shape.area():.2f}”)
print(f”Perimeter: {shape.perimeter():.2f}”)
# Same function works with different shape types
shapes = [Circle(5), Square(4), Rectangle(3, 6)]
for shape in shapes:
print_shape_info(shape)
print(“—“)
🕯️ Magic Note
Python’s polymorphism is “duck typing”: If it walks like a duck and quacks like a duck, it is a duck. You do not need inheritance. If an object has the required methods, it works. This is more flexible than traditional polymorphism.
Python
class Camera:
def click(self):
return “📷 Click!”
class Mouse:
def click(self):
return “🖱️ Click!”
class Button:
def click(self):
return “🔘 Button pressed!”
# No inheritance needed! Just the same method name
def perform_click(thing):
print(thing.click())
perform_click(Camera()) # 📷 Click!
perform_click(Mouse()) # 🖱️ Click!
perform_click(Button()) # 🔘 Button pressed!
Python
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def refund(self, amount):
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount):
# Complex credit card logic here
return f”Processing ${amount} via Credit Card”
def refund(self, amount):
return f”Refunding ${amount} to Credit Card”
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
# Complex PayPal logic here
return f”Processing ${amount} via PayPal”
def refund(self, amount):
return f”Refunding ${amount} to PayPal”
# Client code only depends on the abstract interface
def checkout(processor, amount):
print(processor.process_payment(amount))
checkout(CreditCardProcessor(), 100)
checkout(PayPalProcessor(), 50)
🕯️ Magic Note
Abstract base classes cannot be instantiated. They define a contract that subclasses must fulfill. This is useful for large systems where different teams implement different parts of an interface.
Python
class Engine:
def start(self):
return “Engine started”
def stop(self):
return “Engine stopped”
class Wheels:
def rotate(self):
return “Wheels rotating”
class Seats:
def adjust(self, position):
return f”Seat adjusted to {position}”
# Car is composed of parts (has-a relationship)
class Car:
def __init__(self):
self.engine = Engine()
self.wheels = Wheels()
self.seats = Seats()
def drive(self):
return f”{self.engine.start()}, {self.wheels.rotate()}”
def stop(self):
return self.engine.stop()
my_car = Car()
print(my_car.drive()) # Engine started, Wheels rotating
print(my_car.seats.adjust(“driver position”)) # Seat adjusted to driver position
| Inheritance | Composition |
|---|---|
| “Is-a” relationship | “Has-a” relationship |
| Defined at compile time (static) | Can be changed at runtime (dynamic) |
| Tighter coupling | Loose coupling |
| Can lead to fragile base class problem | More resilient to changes |
| Code reuse through extension | Code reuse through delegation |
| Parent class changes affect all children | Changing part is isolated |
Python
# Composition allows runtime changes (more flexible)
class Logger:
def log(self, message):
print(f”LOG: {message}”)
class FileLogger:
def log(self, message):
with open(“app.log”, “a”) as f:
f.write(f”{message}\n”)
class Application:
def __init__(self, logger):
self.logger = logger # Inject dependency (composition)
def run(self):
self.logger.log(“Application started”)
# Can switch logging behavior easily
app1 = Application(Logger())
app2 = Application(FileLogger())
app1.run() # Prints to console
app2.run() # Writes to file
Python
class A:
def who(self):
return “A”
class B(A):
def who(self):
return “B”
class C(A):
def who(self):
return “C”
class D(B, C):
pass
d = D()
print(d.who()) # B (MRO: D → B → C → A)
# View the MRO
print(D.__mro__)
# (<class ‘D’>, <class ‘B’>, <class ‘C’>, <class ‘A’>, <class ‘object’>)
🕯️ Magic Note
Python uses the C3 linearization algorithm to compute the MRO. It ensures that subclasses come before parents, and the order of parent classes is respected. You can view the MRO using Class.__mro__ or Class.mro().
- Overusing inheritance when composition would be better
- Creating inheritance hierarchies that are too deep (more than 3-4 levels)
- Forgetting to call super().__init__() in child classes
- Using multiple inheritance without understanding MRO
- Breaking encapsulation by accessing private attributes directly
- Creating huge abstract classes that try to do everything
- What is the difference between _name and __name?
- Write a class SavingsAccount that inherits from BankAccount and adds interest.
- What is duck typing? Give an example.
- When should you use composition instead of inheritance?
- What is MRO and how can you view it?
- Write an abstract class Shape with an abstract method area().
⚡ Whisper
Encapsulation is the wall around your data. Inheritance is the bridge between classes. Polymorphism is the many faces of a single name. Abstraction is the mask hiding complexity. Composition is the art of building with parts. These are not just techniques. They are ways of thinking. When you design a system, ask: What should be hidden? What can be reused? Where do I need flexibility? What is the simplest interface? How do parts combine? The answers shape your code. A well-encapsulated class is a fortress. A well-designed inheritance tree is a family. A polymorphic interface is a promise kept in many ways. An abstraction is a gift of simplicity. Composition is a workshop of cooperation. Master these concepts, and you master the mind of an object-oriented programmer. The syntax is small. The ideas are large. Learn them deeply.