So far, functions have been the main way to organize code, break a problem into small, reusable pieces of logic. But some problems aren't just about logic; they're about things a customer, a bank account, a game character that have both data and behavior bundled together. The data needs to persist and change over time. Object-oriented programming (OOP) is the approach Python offers for exactly that. This guide covers the core ideas and why they exist.
The Problem OOP Solves
Imagine tracking a bank account using just variables and functions:
balance = 1000
def deposit(amount):
global balance
balance += amount
def withdraw(amount):
global balance
balance -= amount
This works for one account. What happens with a hundred accounts? You would need a hundred separate balance variables, and functions that somehow know which one to update. The data (balance) and behavior (deposit, withdraw) are not connected to each other they are just nearby.
OOP fixes this by bundling data and behavior into a single unit known as object. The approach rests on four core ideas, known as;
Four Pillars of OOP
Encapsulation
Inheritance
Polymorphism
Abstraction
Classes and Object
A class is a blueprint for creating objects. An object is a specific thing built from that blueprint.
Real World Analogy
Think of a class as an architect's blueprint for a house. It describes what every house built from it will have (rooms, doors, windows) and what it can do ( open the door, turn on lights) but you cannot live in a blueprint, it is just a plan.
An object is an actual plan built from the blueprint. You can build many houses from the same plan, and each has its own separate attribute: one is painted white, another is painted red and turning on lights does not affect the other.
1. Class Attributes
They are defined directly inside the class body. They are shared by all objects of that class unless an individual object overrides them.
class Car:
wheels = 4 # class attribute (shared by all cars)
2. Instance Attributes
They are defined inside methods like the __init__ constructor using self. Each object has its own independent copy.
class House: # the blueprint
def __init__(self, color, rooms):
self.color = color # attributes: what a house has
self.rooms = rooms
Method
Methods are functions defined inside a class. They define/describe the behaviors an object can perform. Like __init__, regular methods take 'self' as their first
parameter so they can access and modify the object's own attributes.
Bank Example
class BankAccount:
# __init__ runs when a new object is created and sets up its instance.
def __init__(self, phone_number, owner_name, balance):
self.phone_number = phone_number
self.owner_name = owner_name
self.balance = balance
# method: calculate amount deposited
def deposit(self, amount):
self.balance += amount # self.balance = self.balance + amount
print(f"You have successfully deposited KES {amount}, new account balance KES {self.balance}")
# method: calculates amount withdrawn
def withdraw(self, amount):
self.balance -= amount # self.balance = self.balance - amount
print(f"You have successfully withdrawn KES {amount}, new account balance KES {self.balance}")
# method: checks balance
def check_balance(self):
print(f"Balance: KES {self.balance}")
# creating instances(object)
maureenacc = BankAccount("+254712345678", "Maureen", 500)
maureenacc.deposit(700)
maureenacc.withdraw(180)
maureenacc.check_balance()
Output
You have successfully deposited KES 700, new acc balance KES 1200
You have successfully withdrawn KES 180, new acc balance KES 1020
Balance: KES 1020
Student Example
class Student:
# __init__ runs when a new object is created and sets up its instance attributes
def __init__(self, name, age):
self.name = name # instance attribute
self.age = age # instance attribute
self.grades = [] # instance attribute (starts empty)
# method: adds a grade to this student's list
def add_grade(self, grade):
self.grades.append(grade)
# method: calculates this student's average
def average(self):
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
# method: returns a summary of this student
def describe(self):
return f"{self.name} (age {self.age}) has an average of {self.average():.1f}"
# creating instances (objects)
student1 = Student("Amina", 20)
student2 = Student("Dennis", 22)
student1.add_grade(80)
student1.add_grade(90)
student2.add_grade(70)
print(student1.describe())
print(student2.describe())
Output
Amina (age 20) has an average of 85.0
Dennis (age 22) has an average of 70.0
Encapsulation: Keeping Data and Behavior Together
Encapsulation means bundling an object's data (attributes) and the methods that are allowed to modify it into a single class, and restricting direct access of some of that data from outside the class.
In python, encapsulation is a convention, not a hard restriction.
-
name- public, accessible from anywhere. -
_name- protected, a signal to other developers "Don't touch this directly". -
__name- private, Python renames it internally(name mangling) to discourage outside access.
class BankAccount:
def __init__(self,owner_name, phone_number, balance=0):
self.owner_name = owner_name
self.phone_number = phone_number
self.__balance = balance
def deposit(self, amount):
if amount <= 40:
print("Deposit must be a minimum of KES 50.")
else:
self.__balance += amount
print(f"You have succesfully deposited {amount} and account balance is {self.__balance}")
def withdraw(self, amount):
if amount > self.__balance:
print("Insufficient funds")
else:
self.__balance -= amount
print(f"You have successfully withdrawn {amount} and account balance is {self.__balance}")
def check_balance(self):
return f"Your account balance is KES {self.__balance}"
maureen_acc = BankAccount("Maureen","0712345678")
maureen_acc.withdraw(500)
print(maureen_acc.check_balance())
Output
Insufficient funds
Your account balance is KES 0
Inheritance: Building on Existing Class
Inheritance allows a new class to (child class/ subclass) reuse the attributes and methods of an existing (parent class/ super class).
# Parent class
class PublicService:
def __init__(self, reg_number, route):
self.reg_number = reg_number
self.route = route
def describe(self):
return f"Vehicle registration number {self.reg_number} plies {self.route} route."
def collect_fares(self, amount):
return f"KES {amount} fare collected on {self.reg_number}"
# child class 1
class Matatu(PublicService):
def __init__(self, reg_number, route, sacco_name, capacity):
super().__init__(reg_number, route)
self.sacco_name = sacco_name
self.capacity = capacity
def describe(self):
return f"Vehicle registration number {self.reg_number}, of sacco {self.sacco_name} plies the {self.route} route."
kacose = Matatu("KBZ 106G","Nairobi-Ruaka","Kacose sacco",14)
print(kacose.describe())
print(kacose.collect_fares(100))
Output
Vehicle registration number KBZ 106G, of sacco Kacose sacco plies the Nairobi-Ruaka route.
KES 100 fare collected on KBZ 106G
Matatu automatically gets everything PublicService already does collect_fare without rewriting it. super().__init__(...) calls the parent class's setup logic, so you don't have to duplicate.
This matters because it captures a real relationship: a matatu is a public service vehicle, with some extra behavior. Inheritance lets your code reflect that relationship directly, instead of copy-pasting the shared parts.
Polymorphism: Same Method Name, Different Behavior
Polymorphism is having many forms. It is the ability of different objects to respond to same method name in their own unique way.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
animals = [Dog(), Cat(), Dog()]
for animal in animals:
print(animal.speak())
# Woof!
# Meow!
# Woof!
The loop doesn't care whether animal is a Dog or a Cat, it just calls speak() and trusts each object to know how to respond correctly. This is what makes OOP scale well: you can add a new Bird class with its own speak() method, and the loop above works with it unmodified.
Abstraction: Defining What, Not How
Abstraction is defining a common interface that a subclass must follow without dictating how each one fulfills it. In Python, this is usually done with the abc module (short for "abstract base class"). ABC serves as a strict blueprint for other classes. It is used alongside @abstractmethod decorator.
from abc import ABC, abstractmethod
class Loan(ABC):
def __init__(self, principal):
self.principal = principal
@abstractmethod
def calculate_interest(self):
pass
def total_repayable(self):
"""This method uses calculate_interest() without knowing how, each loan compute it"""
return self.principal + self.calculate_interest()
# childclass 1
class MpesaLoan(Loan):
def calculate_interest(self):
return self.principal * 0.12
# childclass 2
class SaccoLoan(Loan):
def calculate_interest(self):
return self.principal * 0.15
#childclass 3
class Overdraft(Loan):
def calculate_interest(self):
return self.principal * 0.08
mpesa_loan1 = MpesaLoan(3000)
print(mpesa_loan1.calculate_interest())
print(mpesa_loan1.total_repayable())
sacco_loan1 = SaccoLoan(5000)
print(sacco_loan1.calculate_interest())
print(sacco_loan1.total_repayable())
Output
360.0
3360.0
750.0
5750.0
Loan defines what every loan must be able to do, calculate interest and total repayable, without specifying how because calculation is different for every loan.
Loanitself can never be instantiated.Loanraises aTypeError, because it is a template, not a usable object in its own right.Every subclass is required to implement each
@abstractmethod. IfMpesaLoanforgot to definecalculate_interest, Python would refuse to let you create aMpesaLoaninstance at all.
Comparing The Four Pillars
| Pillar | What It Does | Problem It Solves | Python Example |
|---|---|---|---|
| Encapsulation | Bundles data with the methods allowed to change it | Prevents data from being modified in inconsistent or unsafe ways |
self.balance, routing changes through deposit(),withdraw()
|
| Inheritance | Lets a class reuse and extend another class's attributes and methods | Avoids duplicating code across related classes |
class Matatu(PublicService),super().init__()
|
| Polymorphism | Lets different classes respond to the same method call in their own way | Lets calling code work with many types without checking which one it has |
animal.speak() behaving differently for Dog and Cat
|
| Abstraction | Defines a required interface without dictating the implementation | Guarantees every subclass provides certain behavior, catching gaps early |
@abstractmethod, class Loan(ABC)
|
When OOP Is (and Isn't) The Right Tool
OOP shines when your program deals with entities that have both persistent state and behavior tied to the state. It is less useful for simple, stateless transformations, where a plain function is clearer and easier to test
# no need for a class here — this is a stateless calculation
def celsius_to_fahrenheit(c):
return c * 9 / 5 + 32
A good rule of thumb: reach for a class when you find yourself passing the same group of related data into function after function. That's usually a sign the data and the functions belong together as an object.
Conclusion
Object-oriented programming is a way of organizing code around the things your program models, rather than just the steps it performs. The four pillars work together to make this possible: encapsulation bundles data with the operations allowed to change it, inheritance lets you build specialized versions of existing ideas without duplicating code, polymorphism lets different objects respond to the same request in their own way, and abstraction defines what a class must do without locking in how. Used well, OOP makes code that mirrors how you'd describe the problem in plain language, which is usually a sign it will be easier to read, extend, and maintain later.
Top comments (0)