Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Intermediate 🕑 12 min read 👁 2 views 📅 Oct 22, 2026

Classes and Objects: Modelling Real Things

Learn object-oriented programming in Python with classes, objects, __init__, self and methods. Model train tickets, a bank account with rules and a coach booking system.

Every Ticket Looks the Same, but Every Ticket Is Different

Look at any train ticket. It always has the same kind of details: a PNR number, a passenger name, a seat, a fare and a status such as “Confirmed”. Yet your ticket and your friend’s ticket have different values in those boxes. The railway does not design a new ticket for every passenger. It has one design and fills it in millions of times.

Programming has the same idea. A class is the design: it says what details something has and what it can do. An object (also called an instance) is one filled-in copy, like one real ticket. Writing programs this way is called object-oriented programming, or OOP.

You have already used objects without knowing it. Every string is an object of the class str, and "hello".upper() calls one of its methods. In this lesson you will create your own classes for train tickets, a bank account and a train coach.

Your First Class

A class starts with the keyword class and a name written in CapitalWords style, such as Ticket or BankAccount. Inside it, the special method __init__ (two underscores on each side, said “dunder init”) runs automatically every time you create a new object. Its job is to store the starting details.

class Ticket:
    def __init__(self, pnr, name, seat):
        self.pnr = pnr
        self.name = name
        self.seat = seat


ticket1 = Ticket("2401556789", "Ananya Gupta", "B2-34")
ticket2 = Ticket("2401556790", "Rahul Menon", "B2-35")

print(ticket1.name)    # Ananya Gupta
print(ticket2.name)    # Rahul Menon
print(ticket1.seat)    # B2-34

ticket2.seat = "B2-40"    # objects can be changed
print(ticket2.seat)    # B2-40

Here is what happens on the line ticket1 = Ticket("2401556789", "Ananya Gupta", "B2-34"):

  1. Python creates a new, empty Ticket object.
  2. It calls __init__, passing the new object as self and your three values as pnr, name and seat.
  3. Lines like self.name = name store each value on that object. These stored values are called attributes.
  4. The finished object is saved in ticket1.

What is self?

self means “this particular object”. When __init__ runs for Ananya’s ticket, self is Ananya’s ticket; when it runs for Rahul’s, self is Rahul’s. This is how one design can fill in many different tickets. You never pass self yourself: Python passes it for you. The name self is a convention, not a keyword, but every Python programmer uses it, so you should too.

Methods: What an Object Can Do

A method is a function written inside a class. Its first parameter is always self, so it can read and change that object’s attributes. You call it with a dot: ticket.cancel().

class Ticket:
    def __init__(self, pnr, name, seat, fare):
        self.pnr = pnr
        self.name = name
        self.seat = seat
        self.fare = fare
        self.status = "Confirmed"

    def summary(self):
        return f"PNR {self.pnr}: {self.name}, seat {self.seat}, {self.status}"

    def cancel(self):
        self.status = "Cancelled"
        refund = self.fare * 0.75
        return refund


ticket = Ticket("2401556789", "Ananya Gupta", "B2-34", 1240)
print(ticket.summary())

refund = ticket.cancel()
print(f"Refund: Rs {refund}")     # Refund: Rs 930.0
print(ticket.summary())

Notice that __init__ can also set attributes that are not passed in, like self.status = "Confirmed". Then cancel() changes the status only for this ticket and returns the refund. The data (attributes) and the actions (methods) live together in one place. That is the main idea of OOP.

Creating Many Objects

Once you have a class, you can create as many objects as you need and keep them in a list, just like numbers or strings. Each one keeps its own data.

class Ticket:
    def __init__(self, pnr, name, seat):
        self.pnr = pnr
        self.name = name
        self.seat = seat


bookings = [
    Ticket("2401556789", "Ananya Gupta", "B2-34"),
    Ticket("2401556790", "Rahul Menon", "B2-35"),
    Ticket("2401556791", "Sunita Devi", "S4-12"),
]

for t in bookings:
    print(t.pnr, t.name, t.seat)


def find_ticket(pnr):
    for t in bookings:
        if t.pnr == pnr:
            return t
    return None


found = find_ticket("2401556791")
print("Found:", found.name)    # Found: Sunita Devi

A Bank Account with Rules

Classes are perfect for things that must follow rules. A bank account must never accept a negative deposit or allow you to withdraw more than you have. If the checks live inside the methods, every part of your program gets them for free. When a rule is broken, the method raises a ValueError, which you learned to catch in the Error Handling lesson.

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

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be more than zero")
        self.balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal must be more than zero")
        if amount > self.balance:
            raise ValueError(f"Not enough balance. Available: Rs {self.balance}")
        self.balance -= amount


account = BankAccount("Kiran Patil", 5000)
account.deposit(2000)
account.withdraw(1500)
print(account.balance)    # 5500

try:
    account.withdraw(9000)
except ValueError as error:
    print("Refused:", error)    # Refused: Not enough balance. Available: Rs 5500

try:
    account.deposit(-100)
except ValueError as error:
    print("Refused:", error)    # Refused: Deposit must be more than zero

Notice balance=0 in __init__: default values work in methods just as in normal functions, so BankAccount("Kiran Patil") starts with Rs 0.

Class Attributes and Instance Attributes

Some details are different on every ticket, like the name. Others are the same for all tickets, like the name of the railway. Python lets you store both:

Instance attributeClass attribute
Where you create itInside a method, with self.name = ...Directly in the class body
Belongs toOne objectThe class, shared by all objects
Exampleself.pnr, self.seatrailway, tickets_booked
class Ticket:
    railway = "Indian Railways"    # class attribute: shared by all tickets
    tickets_booked = 0             # class attribute: one counter for the class

    def __init__(self, pnr, name):
        self.pnr = pnr             # instance attributes: different for each ticket
        self.name = name
        Ticket.tickets_booked += 1


t1 = Ticket("2401556789", "Ananya Gupta")
t2 = Ticket("2401556790", "Rahul Menon")

print(t1.railway, "|", t2.railway)      # Indian Railways | Indian Railways
print(t1.name, "|", t2.name)            # Ananya Gupta | Rahul Menon
print("Booked so far:", Ticket.tickets_booked)    # Booked so far: 2

To change a class attribute, write it through the class name: Ticket.tickets_booked += 1. If you wrote self.tickets_booked += 1, Python would create a new instance attribute on that one ticket, and the shared counter would stay at 0.

Friendly Printing with __str__

If you print an object from your own class, Python shows something like <__main__.Ticket object at 0x7f3a...>. That is not useful. Add a __str__ method that returns a string, and Python uses it whenever you call print() or str() on the object.

class Ticket:
    def __init__(self, pnr, name, seat):
        self.pnr = pnr
        self.name = name
        self.seat = seat

    def __str__(self):
        return f"Ticket {self.pnr} | {self.name} | Seat {self.seat}"


ticket = Ticket("2401556789", "Ananya Gupta", "B2-34")
print(ticket)                # Ticket 2401556789 | Ananya Gupta | Seat B2-34
message = "Your booking: " + str(ticket)
print(message)

Dataclasses: Less Typing for Simple Classes

Many classes mainly hold data, and writing self.x = x for every attribute gets boring. The dataclasses module can write __init__, a readable printout and an equality check for you. You list the attributes with their types, and you can still add your own methods.

from dataclasses import dataclass


@dataclass
class Passenger:
    name: str
    age: int
    berth: str = "No preference"

    def is_senior(self):
        return self.age >= 60


p1 = Passenger("Lakshmi Iyer", 64, "Lower")
p2 = Passenger("Aman Joshi", 21)

print(p1)              # Passenger(name='Lakshmi Iyer', age=64, berth='Lower')
print(p2.berth)        # No preference
print(p1.is_senior())  # True
print(p1 == Passenger("Lakshmi Iyer", 64, "Lower"))   # True

The @dataclass line above the class is a decorator. You will learn how decorators work in the Decorators lesson. For now, just remember that dataclasses are great for simple records such as passengers, products or students.

Real-Life Example: Booking Seats in a Coach

Now let us model something closer to a real booking system. Each Coach knows its code, how many seats it has, the fare, who has booked and who is on the waiting list. When the coach is full, new passengers go on the waiting list, just like WL1 and WL2 on a real ticket. We use a tiny coach with only 3 seats so you can see the waiting list quickly.

class Coach:
    def __init__(self, code, total_seats, fare):
        self.code = code
        self.total_seats = total_seats
        self.fare = fare
        self.booked = {}          # seat number -> passenger name
        self.waiting = []         # names on the waiting list

    def seats_left(self):
        return self.total_seats - len(self.booked)

    def book(self, name):
        if self.seats_left() == 0:
            self.waiting.append(name)
            return f"{name}: waiting list WL{len(self.waiting)}"
        seat = len(self.booked) + 1
        self.booked[seat] = name
        return f"{name}: confirmed, seat {self.code}-{seat}, Rs {self.fare}"

    def __str__(self):
        return (f"Coach {self.code}: {len(self.booked)} booked, "
                f"{self.seats_left()} free, {len(self.waiting)} waiting")


sleeper = Coach("S1", 3, 485)
ac_coach = Coach("B1", 2, 1240)

for name in ["Ananya", "Rahul", "Sunita", "Farhan"]:
    print(sleeper.book(name))

print(ac_coach.book("Meena"))

print(sleeper)
print(ac_coach)

Ananya, Rahul and Sunita get seats S1-1, S1-2 and S1-3, and Farhan gets WL1. The two coaches are separate objects, so booking in the sleeper coach does not change the AC coach.

Try these changes:

  1. Add a cancel(seat) method that removes a booking and gives the seat to the first person on the waiting list.
  2. Add a class attribute train_name = "Festival Special" and show it in __str__.
  3. Add a total_collected() method that returns the number of booked seats multiplied by the fare.

Common Mistakes

  • Forgetting self in a method. Python always passes the object as the first argument, so a method with no parameters gets one argument it did not expect:
class Ticket:
    def __init__(self, pnr):
        self.pnr = pnr

    def show():
        print("PNR:", self.pnr)


ticket = Ticket("2401556789")
ticket.show()   # ERROR: TypeError

Fix: add self as the first parameter of every method:

class Ticket:
    def __init__(self, pnr):
        self.pnr = pnr

    def show(self):
        print("PNR:", self.pnr)


ticket = Ticket("2401556789")
ticket.show()   # PNR: 2401556789
  • Forgetting self. when saving an attribute. Writing name = name inside __init__ only creates a local variable that disappears. Later, ticket.name raises an AttributeError. Fix: write self.name = name.
  • Misspelling __init__. _init_ (one underscore) or __int__ is just a normal method, so Python never calls it. Creating the object then fails with “takes no arguments”. Fix: two underscores, the word init, two underscores.
  • Using a list as a class attribute. If you write waiting = [] in the class body, all coaches share one list, and a name added to one coach appears in every coach. Fix: create lists and dictionaries in __init__ with self.waiting = [].
  • __str__ that prints instead of returning. __str__ must return a string. If it only prints, print(ticket) raises a TypeError.

Practice Task: A Mobile Recharge Wallet

Build a RechargeWallet class for a mobile recharge app. Complete the TODO steps in the starter code.

  • __init__ stores the owner, the balance and an empty history list.
  • add_money(amount) refuses zero or negative amounts with a ValueError.
  • recharge(mobile_plan, price) refuses a plan that costs more than the balance.
  • Both methods add a tuple to history.
  • print(wallet) shows Wallet of Neha: Rs 201 after the example calls.
class RechargeWallet:
    def __init__(self, owner, balance=0):
        # TODO 1: save owner and balance, and start an empty list called history
        pass

    def add_money(self, amount):
        # TODO 2: refuse amounts of zero or less with ValueError
        # TODO 3: increase the balance and add ("Added", amount) to history
        pass

    def recharge(self, mobile_plan, price):
        # TODO 4: refuse if price is more than the balance (ValueError)
        # TODO 5: reduce the balance and add (mobile_plan, price) to history
        pass

    # TODO 6: write __str__ so print(wallet) shows "Wallet of Neha: Rs 201"


# When you have finished, remove the # from the lines below and run:
# wallet = RechargeWallet("Neha")
# wallet.add_money(500)
# wallet.recharge("28-day plan", 299)
# print(wallet)
# print(wallet.history)
Show one possible answer (try it yourself first!)
class RechargeWallet:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.history = []

    def add_money(self, amount):
        if amount <= 0:
            raise ValueError("Amount must be more than zero")
        self.balance += amount
        self.history.append(("Added", amount))

    def recharge(self, mobile_plan, price):
        if price > self.balance:
            raise ValueError(f"Not enough balance for {mobile_plan}")
        self.balance -= price
        self.history.append((mobile_plan, price))

    def __str__(self):
        return f"Wallet of {self.owner}: Rs {self.balance}"


wallet = RechargeWallet("Neha")
wallet.add_money(500)
wallet.recharge("28-day plan", 299)
print(wallet)            # Wallet of Neha: Rs 201
print(wallet.history)    # [('Added', 500), ('28-day plan', 299)]

try:
    wallet.recharge("84-day plan", 719)
except ValueError as error:
    print("Refused:", error)   # Refused: Not enough balance for 84-day plan

Key Takeaways

  • A class is a design; an object is one filled-in copy. Create one with ClassName(...).
  • __init__ runs when an object is created and stores its attributes with self.name = value.
  • self is the object a method is working on. Every method needs it as its first parameter.
  • Put rules inside methods (like checks in withdraw) so the object can protect its own data.
  • Class attributes are shared by all objects; instance attributes belong to one object.
  • Add __str__ for friendly printing, and use @dataclass for simple data-holding classes.

❓ Frequently Asked Questions

A class is the design that describes what details and actions something has. An object is one real copy made from that design, with its own values.
No, it is only a strong convention. Python passes the object as the first argument of every method, and everyone names that parameter self so code is easy to read.
People often call it that. Strictly, Python creates the object first and then calls __init__ to set it up, but for everyday code you can think of it as the constructor.
Use a dataclass when the class mainly stores data, such as a student or a product. Use a normal class when you need more control over how objects are set up.
No. Functions are enough for many small scripts. Classes become helpful when you have many things of the same kind, each with its own data and rules.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋