As I continue navigating Python and data science, one concept that has helped me understand how to structure programs better is Object-Oriented Programming (OOP).
At first, working with variables, lists, dictionaries, loops and functions was enough for the small programs I was writing. However, as programs become more complex, keeping related data and functions organized becomes increasingly important.
That is where Object-Oriented Programming comes in. In this article, I will walk through the fundamentals of OOP in Python, including classes, objects, attributes, methods and the four pillars of OOP.
1. What is Object-Oriented Programming?
OOP is a programming approach that organizes code around objects. An object combines related data and behavior into one unit.
For example, think about a car. A car has attributes such as:
- Brand
- Color
- Model
- Maximum speed
It also has behaviors, such as:
- Starting
- Stopping
- Accelerating
Instead of storing all these details in separate variables and writing unrelated functions, OOP allows us to group them together in a single object.
Why OOP?
There are three main benefits I found useful when learning this concept.
1. Organization
Related data and functions stay together, making code easier to understand and navigate.
2. Reusability
Once you define a class, you can create multiple objects from it without rewriting the same structure.
3. Maintainability
When you need to change how a particular behavior works, you can usually make that change in one place.
2. Classes and Objects
Before writing our first OOP program, we need to understand two fundamental concepts: classes and objects.
What is a class?
A class is a blueprint or template used to create objects. It defines the attributes and methods that objects of that class can have.
For example, a class called Car can describe the common characteristics and behaviors of different cars.
In Python, we define a class using the class keyword.
class Car:
pass
The pass keyword allows us to define an empty class temporarily.
What is an object?
An object is an instance of a class. If Car is the blueprint, individual cars created from that blueprint are objects.
For example, a BMW X5 and a Volvo XC60 can both be objects created from the same Car class, even though they have different attributes.
Let's see how this works.
class Car:
def __init__(self, brand, color, model):
self.brand = brand
self.color = color
self.model = model
car1 = Car("BMW", "Blue", "X5")
car2 = Car("Volvo", "Grey", "XC60")
print(car1.brand)
print(car2.brand)
Output:
BMW
Volvo
Both objects come from the same class, but each stores its own information.
Understanding __init__()
The __init__() method runs automatically when a new object is created. We commonly use it to initialize an object's attributes.
For example:
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
dog1 = Dog("Bosco", "African Shepherd")
dog2 = Dog("Max", "German Shepherd")
print(dog1.name)
print(dog2.name)
Output:
Bosco
Max
When we create dog1, Python calls __init__() and assigns the supplied values to that object. The same happens when we create dog2. You may hear __init__() described as a constructor. More precisely, Python uses __new__() to create an instance, while __init__() initializes it.
What is self?
The self parameter refers to the particular object whose method is being called. In a regular instance method, self is the first parameter. Python supplies the instance automatically when you call the method.
For example:
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says: Woof!")
dog1 = Dog("Bosco")
dog2 = Dog("Max")
dog1.bark()
dog2.bark()
Output:
Bosco says: Woof!
Max says: Woof!
The method is the same, but self.name refers to the name belonging to whichever dog is calling it.
3. Attributes and Methods
Attributes
Attributes are variables associated with a class or an object. In Python, we commonly distinguish between instance attributes and class attributes.
Instance attributes
These belong to individual objects. They are often initialized inside __init__() using self.
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
Here, name and breed are instance attributes. Each dog can have its own values.
Class attributes
These are defined directly inside the class body, outside instance methods. They are shared through the class unless an instance provides its own attribute with the same name.
class Dog:
species = "Canis familiaris"
def __init__(self, name, breed):
self.name = name
self.breed = breed
dog1 = Dog("Bosco", "African Shepherd")
dog2 = Dog("Max", "German Shepherd")
print(dog1.species)
print(dog2.species)
Output:
Canis familiaris
Canis familiaris
Both dogs share the same species classification, but their names and breeds differ.
Methods
Methods are functions defined inside a class. They describe the behaviors an object can perform.
Let's expand our Dog class.
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
print(f"{self.name} says: Woof!")
def play(self, minutes):
print(f"{self.name} played for {minutes} minutes")
def fact(self, top_speed):
print(
f"{self.name}, a {self.breed}, "
f"can reach {top_speed} km/h."
)
dog1 = Dog("Bosco", "African Shepherd")
dog1.bark()
dog1.play(30)
dog1.fact(72)
Output:
Bosco says: Woof!
Bosco played for 30 minutes
Bosco, a African Shepherd, can reach 72 km/h.
This example brings together the concepts covered so far. The class defines the structure, the object stores the individual dog's information and the methods define its behaviors.
4. Python Naming Conventions
Consistent naming makes code easier to read, especially when working on larger projects.
| Element | Convention | Example |
|---|---|---|
| Variables | snake_case |
owner_name |
| Functions | snake_case |
check_balance() |
| Classes | PascalCase |
MpesaAccount |
| Constants | UPPER_SNAKE_CASE |
API_KEY |
Python does not enforce every naming convention, but following these conventions helps keep code readable and consistent.
5. The Four Pillars of OOP
The four commonly recognized pillars of Object-Oriented Programming are:
- Encapsulation
- Inheritance
- Polymorphism
- Abstraction
Let's explore each one.
5.1 Encapsulation
Encapsulation means grouping data and the methods that operate on that data inside a class, while controlling how that data is accessed or modified. Think about a bank account. You should not be able to change your account balance arbitrarily without following the bank's rules. Instead, you interact with the account through operations such as depositing, withdrawing and checking your balance.
Python uses naming conventions and mechanisms to support encapsulation:
-
name: Public attribute, intended for general access. -
_name: Non-public attribute by convention. It signals that the attribute should generally be treated as an internal implementation detail. -
__name: An attribute using double-leading underscores. Python applies name mangling to make accidental access from outside the class less likely.
These conventions do not provide absolute security, but they help structure code.
Example: A bank account
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance
def deposit(self, amount):
if amount <= 0:
print("Deposit amount must be positive.")
return
self.__balance += amount
print(f"Deposited KES {amount}.")
def withdraw(self, amount):
if amount <= 0:
print("Withdrawal amount must be positive.")
return
if amount > self.__balance:
print("Insufficient funds.")
return
self.__balance -= amount
print(f"Withdrew KES {amount}.")
def check_balance(self):
return self.__balance
account = BankAccount("Sylvia", 3000)
account.deposit(100)
account.withdraw(1000)
print(account.check_balance())
Output:
Deposited KES 100.
Withdrew KES 1000.
2100
You notice that we use methods to modify the balance instead of directly changing __balance. These methods also validate transactions before updating the account. This is a practical example of encapsulation: the class manages its internal state through controlled operations.
Another example: An M-Pesa account
The same principle applies to a mobile money account.
class MpesaAccount:
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 < 50:
print("Deposit must be at least KES 50.")
return
self.__balance += amount
print(f"New balance: KES {self.__balance}")
def check_balance(self):
return self.__balance
account = MpesaAccount("Sylvia", "0712345678")
account.deposit(150)
print(account.check_balance())
Output:
New balance: KES 150
150
This is a simplified learning example to demonstrate how an object can manage its own data.
5.2 Inheritance
Inheritance allows a new class to reuse attributes and methods from an existing class. The existing class is called the parent class or superclass, while the new class is called the child class or subclass. Inheritance is useful when several types of objects share common characteristics but also have their own specialized behaviors.
For example, cows and chickens are both farm animals. Both have names, ages and weights and both can eat. However, cows produce milk while chickens lay eggs. Instead of defining everything separately, we can create a shared parent class.
Example: Farm animals
class FarmAnimal:
def __init__(self, name, age, weight):
self.name = name
self.age = age
self.weight = weight
def eat(self):
print(f"{self.name} is eating.")
def display_details(self):
print(f"Name: {self.name}")
print(f"Age: {self.age}")
print(f"Weight: {self.weight} kg")
class Cow(FarmAnimal):
def __init__(self, name, age, weight, milk_per_day):
super().__init__(name, age, weight)
self.milk_per_day = milk_per_day
def produce_milk(self):
print(
f"{self.name} produces "
f"{self.milk_per_day} litres of milk per day."
)
class Chicken(FarmAnimal):
def __init__(self, name, age, weight, eggs_per_week):
super().__init__(name, age, weight)
self.eggs_per_week = eggs_per_week
def produce_eggs(self):
print(
f"{self.name} produces "
f"{self.eggs_per_week} eggs per week."
)
cow = Cow("Tuimet", 3, 250, 25)
chicken = Chicken("Ingokho", 2, 2, 10)
cow.display_details()
cow.eat()
cow.produce_milk()
chicken.display_details()
chicken.eat()
chicken.produce_eggs()
Here, Cow and Chicken inherit eat() and display_details() from FarmAnimal, so we do not need to rewrite those methods.
What does super() do?
The expression super() allows a child class to access methods from its parent class.
In the example:
super().__init__(name, age, weight)
we call the parent's initializer to set up the attributes that cows and chickens have in common. Each child class then adds its own specialized attribute. This reduces duplication and makes the code easier to maintain.
5.3 Polymorphism
Polymorphism means that different objects can respond to the same method call in different ways. For example, a delivery service may use vans and motorbikes. Both perform deliveries, but their delivery statuses or costs may differ.
To model that idea;
class Delivery:
def __init__(self, customer_name, destination, distance):
self.customer_name = customer_name
self.destination = destination
self.distance = distance
def delivery_status(self):
print("Delivery is processing.")
class Van(Delivery):
def delivery_status(self):
print("The van delivery is on its way.")
class Motorbike(Delivery):
def delivery_status(self):
print("The motorbike delivery is complete.")
van = Van("Wilson", "Mombasa", 400)
motorbike = Motorbike("Emmanuel", "Upper Hill", 2)
van.delivery_status()
motorbike.delivery_status()
Output:
The van delivery is on its way.
The motorbike delivery is complete.
Both objects use the same method name, delivery_status(), but each class implements it differently. This is an example of method overriding, where a child class provides its own implementation of a method inherited from its parent. Polymorphism makes it possible to write code that works with different object types without requiring every object to behave identically.
5.4 Abstraction
Abstraction means hiding unnecessary implementation details and exposing only the operations that a user needs. Consider using an ATM. You can withdraw money, deposit money or check your balance without knowing all the internal processes involved in verifying your account and updating the bank's records. The ATM provides a simple interface while hiding much of the complexity behind it.
In programming, abstraction helps us focus on what an object can do rather than every detail of how it does it.
For example, when we use our bank account class, we can write:
account.deposit(500)
account.withdraw(200)
account.check_balance()
We do not need to repeat the balance calculation every time we make a transaction. The methods provide a simple interface for those operations.
Python also supports formal abstraction through abstract classes and the abc module. That is a useful next step once you are comfortable with classes, inheritance and methods.
6. Putting the Concepts Together
Let's look at a practical example inspired by Kenya's public transport system.
A matatu has information such as its registration number, route, passenger capacity, fare and number of passengers currently on board. It also performs actions such as boarding passengers and calculating potential fare revenue.
We can represent those attributes and behaviors in a class.
class Matatu:
def __init__(
self,
registration_number,
route,
capacity,
fare,
passengers=0
):
self.registration_number = registration_number
self.route = route
self.capacity = capacity
self.fare = fare
self.passengers = passengers
def board_passengers(self, number):
if number <= 0:
print("Enter a valid number of passengers.")
return
if self.passengers + number > self.capacity:
available = self.capacity - self.passengers
print(f"Only {available} seats are available.")
return
self.passengers += number
print(f"{number} passengers have boarded.")
def trip_income(self):
income = self.passengers * self.fare
return income
def display_details(self):
print(f"Registration: {self.registration_number}")
print(f"Route: {self.route}")
print(f"Passengers: {self.passengers}/{self.capacity}")
matatu = Matatu(
"KDA 001A",
"Nairobi-Thika",
32,
100
)
matatu.board_passengers(20)
matatu.display_details()
print(f"Expected fare revenue: KES {matatu.trip_income()}")
Output:
20 passengers have boarded.
Registration: KDA 001A
Route: Nairobi-Thika
Passengers: 20/32
Expected fare revenue: KES 2000
This example brings several OOP concepts together:
-
Class:
Matatudefines the structure of a matatu object. -
Object:
matatuis an instance of that class. - Attributes: The registration number, route, capacity, fare and passenger count store the object's information.
-
Methods:
board_passengers(),trip_income()anddisplay_details()define what the object can do. - Encapsulation: The boarding method checks capacity before changing the passenger count.
We can create other matatu objects with different routes, fares and capacities without rewriting the class.
Conclusion
Learning Object-Oriented Programming has really helped me see how Python can be used to model real-world things and organize related data and behavior. The best way to understand these concepts is to practice building small programs. Start with a Car or Dog class, then move on to something more practical, such as a bank account, inventory manager or transport management system.
I am continuing to build my Python skills one concept at a time and OOP is an important step toward writing more structured, reusable and maintainable programs.
Top comments (0)