DocsHub
Object-Oriented Programming

Classes & Objects

Learn what Object-Oriented Programming is, how to create classes, and how to work with objects in Python.

Classes & Objects

What is Object-Oriented Programming?

Before writing a single line of code, let us understand what OOP actually is and why it exists.

Imagine you are building a banking system. Without OOP, you might write code like this:

owner1 = "Ahmad"
balance1 = 10000

owner2 = "Sara"
balance2 = 5000

def deposit(balance, amount):
    return balance + amount

def withdraw(balance, amount):
    return balance - amount

This works for two accounts. But what about a thousand accounts? Ten thousand? You would have thousands of separate variables with no clear connection between them. The code becomes impossible to manage.

Object-Oriented Programming solves this by letting you model real-world things as objects in your code. A bank account is a real thing — it has data (owner, balance) and it can do things (deposit, withdraw). OOP lets you define that structure once and create as many instances of it as you need.

The four core principles of OOP are:

Encapsulation — bundling data and the functions that work on it together, and hiding internal details from the outside world.

Inheritance — a class can inherit properties and behavior from another class, so you do not repeat yourself.

Polymorphism — different objects can respond to the same action in different ways.

Abstraction — hiding complex implementation details and showing only what is necessary.

You will see all four of these come to life throughout this section — using a real bank account example from start to finish.


What is a class?

A class is a blueprint. It describes what data an object will hold and what actions it can perform. The class itself is not the object — it is the template used to create objects.

Think of it like this — a class is the architectural plan of a house. The plan is not a house. But you can build many houses from the same plan, and each house is its own separate thing.

class BankAccount:
    pass

That is a complete class. pass means the body is empty for now. It does nothing useful yet — but it is a valid class.


What is an object?

An object is an instance of a class. You create it by calling the class like a function:

class BankAccount:
    pass

account1 = BankAccount()
account2 = BankAccount()

print(account1)   # <__main__.BankAccount object at 0x...>
print(account2)   # <__main__.BankAccount object at 0x...>

account1 and account2 are two separate objects — both built from the same BankAccount class, but completely independent of each other.


init — the constructor

Right now our class is empty. We need to give each account its own data — an owner name and a balance. The __init__ method is called automatically every time you create a new object. This is called the constructor.

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

Now when you create a BankAccount, you must pass the owner and balance:

account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara", 5000)

self — what is it?

self refers to the object itself — the specific instance being created or used. When you write self.owner = owner, you are saying — store this owner value on this particular object.

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # this object's owner
        self.balance = balance  # this object's balance

self is always the first parameter of any method in a class. Python passes it automatically — you never pass it yourself when calling:

account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara", 5000)

print(account1.owner)    # Ahmad
print(account2.owner)    # Sara
print(account1.balance)  # 10000
print(account2.balance)  # 5000

Each object has its own owner and balance. They do not interfere with each other.

self is just a convention — technically you could name it anything. But always use self. Every Python developer expects it and anything else will confuse people immediately.


Instance variables

Variables stored on self are called instance variables. Each object has its own copy:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara", 5000)

# changing one does not affect the other
account1.balance = 99999

print(account1.balance)   # 99999
print(account2.balance)   # 5000 — unchanged

Class variables

A class variable is shared across all instances. It belongs to the class itself, not to any individual object.

class BankAccount:
    bank_name = "HBL Bank"   # class variable — shared by all accounts

    def __init__(self, owner, balance):
        self.owner = owner       # instance variable — unique per object
        self.balance = balance   # instance variable — unique per object
account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara", 5000)

print(account1.bank_name)   # HBL Bank
print(account2.bank_name)   # HBL Bank

# change the class variable — affects all instances
BankAccount.bank_name = "MCB Bank"

print(account1.bank_name)   # MCB Bank
print(account2.bank_name)   # MCB Bank

Always change a class variable through the class itself — BankAccount.bank_name = "MCB" — not through an instance. Changing through an instance creates a new instance variable that shadows the class variable, which is almost never what you want.


A complete class — putting it together

class BankAccount:
    bank_name = "HBL Bank"

    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

    def check(self):
        print(f"Bank: {BankAccount.bank_name}")
        print(f"Owner: {self.owner}")
        print(f"Balance: {self.balance} PKR")
        print("-" * 30)


# create two accounts
account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara", 5000)

account1.check()
account2.check()

Output:

Bank: HBL Bank
Owner: Ahmad
Balance: 10000 PKR
------------------------------
Bank: HBL Bank
Owner: Sara
Balance: 5000 PKR
------------------------------

Two separate objects. Same class. Same bank_name. Different owner and balance.


Adding a second class — Car

Let us build a second class alongside BankAccount to see how the same concepts apply:

class Car:
    brand_name = "Tesla"

    def __init__(self, model, price):
        self.model = model
        self.price = price

    def info(self):
        print(f"Brand: {Car.brand_name}")
        print(f"Model: {self.model}")
        print(f"Price: ${self.price}")


car1 = Car("Model S", 80000)
car2 = Car("Model 3", 40000)

car1.info()
car2.info()

Output:

Brand: Tesla
Model: Model S
Price: $80000
Brand: Tesla
Model: Model 3
Price: $40000

Same pattern — class variable for shared data, instance variables for unique data.


How Python finds a variable — instance vs class

When you access self.something or account.something, Python looks in this order:

  1. The instance itself — does this object have this attribute?
  2. The class — does the class have this attribute?
  3. Parent classes — we cover this in inheritance
class BankAccount:
    bank_name = "HBL Bank"   # class level

    def __init__(self, owner, balance):
        self.owner = owner       # instance level
        self.balance = balance   # instance level

account = BankAccount("Ahmad", 10000)

# instance level — found on the object
print(account.owner)      # Ahmad

# class level — not on the object, found on the class
print(account.bank_name)  # HBL Bank

A real example — opening multiple accounts

class BankAccount:
    bank_name = "HBL Bank"
    total_accounts = 0   # tracks how many accounts exist

    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
        BankAccount.total_accounts += 1   # increment on every new account

    def check(self):
        print(f"Bank: {BankAccount.bank_name}")
        print(f"Owner: {self.owner}")
        print(f"Balance: {self.balance:,} PKR")
        print("-" * 30)


account1 = BankAccount("Ahmad", 10000)
account2 = BankAccount("Sara",  25000)
account3 = BankAccount("Omar",  8000)

account1.check()
account2.check()
account3.check()

print(f"Total accounts opened: {BankAccount.total_accounts}")

Output:

Bank: HBL Bank
Owner: Ahmad
Balance: 10,000 PKR
------------------------------
Bank: HBL Bank
Owner: Sara
Balance: 25,000 PKR
------------------------------
Bank: HBL Bank
Owner: Omar
Balance: 8,000 PKR
------------------------------
Total accounts opened: 3

total_accounts is a class variable — every time a new BankAccount is created, it increments. It belongs to the class, not to any single account.


Summary

ConceptMeaning
ClassBlueprint for creating objects
ObjectAn instance of a class
__init__Constructor — runs when object is created
selfRefers to the current object
Instance variableUnique to each object — self.owner
Class variableShared by all objects — BankAccount.bank_name

On this page