Classes & Objects

Python · Lesson 34 of 56 6 min read Updated

So far you have stored data in variables, lists and dictionaries, and written functions that work on that data. A class puts the two together: it describes a kind of thing, the data it holds and the actions it can perform. Each actual thing made from a class is called an object.

Classes are the heart of object-oriented programming (OOP). They help you model real things, such as bank accounts, students or train tickets, in code that is easy to read and reuse. Everything in Python is already an object: a string is an object of class str, and a list is an object of class list.

Defining a class and creating objects

Think of a class as a blueprint and objects as the houses built from it. You write the blueprint once with the class keyword. Class names use CapWords style: BankAccount, CricketPlayer.

PythonRun it yourself
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

priya = BankAccount("Priya", 5000)
rohan = BankAccount("Rohan")

print(priya.owner, priya.balance)
print(rohan.owner, rohan.balance)
print(type(priya))
Output
Priya 5000
Rohan 0
<class '__main__.BankAccount'>

Calling the class like a function, BankAccount("Priya", 5000), creates a new object. Python then runs the special method __init__ (short for initialise) to set the object up. The values stored on the object, owner and balance, are called attributes. You read them with a dot: priya.balance.

What is self?

self is the object being worked on. When you write BankAccount("Priya", 5000), Python creates an empty object and passes it to __init__ as self, followed by your arguments. So self.owner = owner means "store the owner on this object". Every method receives the object as its first parameter, and by convention it is always named self.

Methods: functions that belong to a class

A method is a function defined inside a class. It describes something the object can do, and it uses self to read and change the object's attributes.

PythonRun it yourself
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

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

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(f"{self.owner} has only ₹{self.balance}")
        self.balance -= amount
        return self.balance

    def describe(self):
        return f"{self.owner}: ₹{self.balance:,}"

priya = BankAccount("Priya", 5000)
priya.deposit(2500)
priya.withdraw(1200)
print(priya.describe())

try:
    priya.withdraw(10000)
except ValueError as error:
    print("Error:", error)
Output
Priya: ₹6,300
Error: Priya has only ₹6300

When you call priya.deposit(2500), Python turns it into BankAccount.deposit(priya, 2500) behind the scenes. That is how self gets its value. Each object keeps its own attributes, so depositing into Priya's account never changes Rohan's.

Class attributes and instance attributes

Attributes set on self are instance attributes: each object has its own. Attributes written directly inside the class body are class attributes: one value shared by all objects of the class. They suit constants such as an interest rate or a bank name.

PythonRun it yourself
class SavingsAccount:
    bank = "Bharat Co-operative Bank"
    interest_rate = 3.5          # percent per year, shared by all accounts
    accounts_opened = 0

    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
        SavingsAccount.accounts_opened += 1

    def yearly_interest(self):
        return round(self.balance * self.interest_rate / 100, 2)

a = SavingsAccount("Meera", 20000)
b = SavingsAccount("Kabir", 45000)

print(a.bank)
print(a.yearly_interest(), b.yearly_interest())
print("Accounts:", SavingsAccount.accounts_opened)
Output
Bharat Co-operative Bank
700.0 1575.0
Accounts: 2

When Python looks up self.interest_rate, it checks the object first and then the class, so every account finds the shared rate. The counter is updated through the class name, SavingsAccount.accounts_opened, so all objects share one count.

Properties: attributes with rules

Sometimes an attribute should be calculated, or protected from bad values. The @property decorator turns a method into something you read like an attribute, without brackets. A matching .setter runs your checks whenever someone assigns a new value.

PythonRun it yourself
class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks            # goes through the setter below

    @property
    def marks(self):
        return self._marks

    @marks.setter
    def marks(self, value):
        if not 0 <= value <= 100:
            raise ValueError("Marks must be between 0 and 100")
        self._marks = value

    @property
    def passed(self):
        return self.marks >= 40

isha = Student("Isha", 72)
print(isha.marks, isha.passed)

try:
    isha.marks = 140
except ValueError as error:
    print("Error:", error)
Output
72 True
Error: Marks must be between 0 and 100

The real value lives in self._marks. A single leading underscore is a Python convention meaning "internal, please do not touch from outside". Python does not enforce it, but other programmers respect it.

Class methods and static methods

A normal method works on one object. A class method, marked with @classmethod, receives the class itself as cls. It is often used as an alternative way to create objects, for example from a line of text. A static method, marked with @staticmethod, receives neither; it is a plain helper function that lives inside the class because it belongs there logically.

PythonRun it yourself
class Train:
    def __init__(self, number, name, source):
        self.number = number
        self.name = name
        self.source = source

    @classmethod
    def from_text(cls, line):
        number, name, source = line.split("|")
        return cls(int(number), name, source)

    @staticmethod
    def is_valid_number(number):
        return 10000 <= number <= 99999

t = Train.from_text("12015|Ajmer Shatabdi|New Delhi")
print(t.name, "from", t.source)
print(Train.is_valid_number(t.number))
Output
Ajmer Shatabdi from New Delhi
True

Printing an object directly, as in print(t), shows something unhelpful like <__main__.Train object at 0x...>. You will fix that with __str__ and __repr__ in Special (Dunder) Methods.

Common mistakes

  • Forgetting self in a method definition. def deposit(amount): fails when called with TypeError: BankAccount.deposit() takes 1 positional argument but 2 were given, because Python always passes the object first.
  • Forgetting self. inside a method. Writing balance += amount creates or changes a local variable, not the attribute. Use self.balance.
  • Using a list as a class attribute for per-object data. A class-level items = [] is shared by every object. Create lists in __init__ with self.items = [].
  • Calling a method without brackets. priya.describe gives the method itself; priya.describe() runs it.

Key points

  • A class is a blueprint; objects (instances) are created by calling the class.
  • __init__ sets up a new object, and self refers to the object being worked on.
  • Methods are functions inside a class that receive self first.
  • Instance attributes belong to one object; class attributes are shared by all objects of the class.
  • @property lets you compute or validate attributes; a leading underscore marks internal attributes.
  • @classmethod receives the class (cls) and is handy for alternative constructors; @staticmethod receives neither.

Exercise

Create a class CricketPlayer with attributes name, runs (default 0) and balls (default 0). Add a method add_innings(runs, balls) that adds to both totals, and a property strike_rate that returns runs per 100 balls rounded to 2 places (return 0 if no balls were faced). Create a player Rohan, add innings of 45 off 30 balls and 82 off 61 balls, then print his totals and strike rate.

Show solution
Python
class CricketPlayer:
    def __init__(self, name, runs=0, balls=0):
        self.name = name
        self.runs = runs
        self.balls = balls

    def add_innings(self, runs, balls):
        self.runs += runs
        self.balls += balls

    @property
    def strike_rate(self):
        if self.balls == 0:
            return 0
        return round(self.runs / self.balls * 100, 2)

rohan = CricketPlayer("Rohan")
rohan.add_innings(45, 30)
rohan.add_innings(82, 61)
print(rohan.name, rohan.runs, rohan.balls, rohan.strike_rate)
Output
Rohan 127 91 139.56

strike_rate is calculated from the current totals every time you read it, so it can never be out of date. Because it is a property, you read it without brackets.

Quick Quiz

1. What is self inside a method?

2. When does __init__ run?

3. A class has rate = 3.5 written in its body. What is rate?

4. What does @classmethod give a method as its first parameter?

Learn Python with a mentor

Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call