DEV Community

Emilio Ochieng
Emilio Ochieng

Posted on

Object-Oriented Programming Explained

This article explains OOP through one running example, a simple bank account, in Python.

What is OOP?

OOP is a way of organizing code around objects, which bundle together:

Data (called attributes), such as an account's owner and balance
Behavior (called methods), such as deposit and withdraw

The alternative is writing loose functions that pass data around. That works for small scripts, but as programs grow, it gets hard to see which function is allowed to change which data. OOP keeps related data and behavior in one place.

Classes and objects

A class is a blueprint. An object is something built from that blueprint.

python
class BankAccount:
def init(self, owner, balance=0):
self.owner = owner
self.balance = balance

def deposit(self, amount):
    self.balance += amount

def withdraw(self, amount):
    if amount > self.balance:
        raise ValueError("Insufficient funds")
    self.balance -= amount
Enter fullscreen mode Exit fullscreen mode

ann = BankAccount("Ann", 1000)
ben = BankAccount("Ben")

ann.deposit(500)
print(ann.balance) # 1500
print(ben.balance) # 0

BankAccount is the class. ann and ben are two separate objects, each with their own balance. init is the constructor that runs when you create an object, and self refers to the specific object being worked on.

The four pillars of OOP

  1. Encapsulation

Encapsulation means hiding an object's internal details and controlling access to them. Right now anyone can write ann.balance = 1,000,000, which defeats the purpose of having a withdraw rule.

Python uses a naming convention: a leading underscore means "internal, please don't touch".

python
class BankAccount:
def init(self, owner, balance=0):
self.owner = owner
self._balance = balance

@property
def balance(self):
    return self._balance

def deposit(self, amount):
    if amount <= 0:
        raise ValueError("Deposit must be positive")
    self._balance += amount
Enter fullscreen mode Exit fullscreen mode

Now the balance can be read but not set directly. The only way to change it is through methods that enforce the rules. That is the main payoff of encapsulation: the object protects its own consistency.

2. Abstraction

Abstraction means exposing a simple interface and hiding the complexity behind it. You press a key on your keyboard without knowing how the signal travels through the circuitry.

In code, account.withdraw(200) is the interface. Whether it checks fraud rules, writes to a database or sends an SMS is not the caller's concern, and it can change later without breaking the code that calls it.

3. Inheritance

Inheritance lets a class reuse and extend another class. A savings account is a bank account with some extras:

python
class SavingsAccount(BankAccount):
def init(self, owner, balance=0, rate=0.05):
super().init(owner, balance)
self.rate = rate

def add_interest(self):
    self.deposit(self._balance * self.rate)
Enter fullscreen mode Exit fullscreen mode

SavingsAccount gets deposit, withdraw and balance for free from BankAccount, and adds its own add_interest. This avoids copy-pasting code.

4. Polymorphism

Polymorphism means different objects can respond to the same method call in their own way.

python
class CurrentAccount(BankAccount):
def withdraw(self, amount):
# allows an overdraft of up to 500
if amount > self._balance + 500:
raise ValueError("Overdraft limit exceeded")
self._balance -= amount

accounts = [BankAccount("Ann", 100), CurrentAccount("Ben", 100)]

for acc in accounts:
acc.withdraw(300) # each class applies its own rule

The loop doesn't need to know which kind of account it has. It just calls withdraw, and each object does the right thing. This is what lets you add new account types later without rewriting the code that uses them.

A note on "is-a" versus "has-a"

Inheritance models an "is-a" relationship: a savings account is a bank account. Many beginners overuse it. When the relationship is "has-a", use composition, where one object holds another:

python
class Customer:
def init(self, name):
self.name = name
self.accounts = [] # a customer HAS accounts

A common guideline is to prefer composition over inheritance. Deep inheritance chains become rigid and hard to change.

When OOP is the wrong tool

OOP is useful, but it isn't always the answer:

Small scripts and data pipelines are often clearer as plain functions that transform data from one step to the next.
Over-engineering is a real risk. If a class has one method and no state, it should probably just be a function.
Some problems fit functional approaches better, and many modern codebases mix both styles.

Use OOP when you have things with state and behavior that belong together. Don't use it just because it feels more "professional".

Quick recap
Concept One-line meaning
Class / object Blueprint / the thing built from it
Encapsulation Protect internal data behind methods
Abstraction Show a simple interface, hide the complexity
Inheritance Reuse and extend an existing class
Polymorphism Same method call, different behavior per class

Top comments (0)