DEV Community

Cover image for An Introduction to Object-Oriented Programming (OOP) in Python
Sylvia Ndili
Sylvia Ndili

Posted on

An Introduction to Object-Oriented Programming (OOP) in Python

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

BMW
Volvo
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

Bosco
Max
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Output:

Bosco says: Woof!
Max says: Woof!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

Canis familiaris
Canis familiaris
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

Bosco says: Woof!
Bosco played for 30 minutes
Bosco, a African Shepherd, can reach 72 km/h.
Enter fullscreen mode Exit fullscreen mode

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:

  1. Encapsulation
  2. Inheritance
  3. Polymorphism
  4. 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())
Enter fullscreen mode Exit fullscreen mode

Output:

Deposited KES 100.
Withdrew KES 1000.
2100
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

Output:

New balance: KES 150
150
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Output:

The van delivery is on its way.
The motorbike delivery is complete.
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()}")
Enter fullscreen mode Exit fullscreen mode

Output:

20 passengers have boarded.
Registration: KDA 001A
Route: Nairobi-Thika
Passengers: 20/32
Expected fare revenue: KES 2000
Enter fullscreen mode Exit fullscreen mode

This example brings several OOP concepts together:

  • Class: Matatu defines the structure of a matatu object.
  • Object: matatu is 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() and display_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)