AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Object-Oriented Programming: Thinking in Objects

📚 Advanced Programming Concepts⏱️ 19 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 19 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

One Template, Many Cards

Take out your Aadhaar card, or picture your school ID card. Every Aadhaar card issued by UIDAI has exactly the same layout: a photograph in the same corner, a 12-digit number in the same font, a name field, a date-of-birth field, an address block, and a QR code that works the same way on every card. UIDAI did not design a fresh card layout for each of India's billion-plus cardholders — it designed one template, once, and every person's card is a separate copy of that template filled in with that person's own details. Your card and your friend's card obey identical rules about what information they hold and how the QR code gets scanned, yet the actual name, number, and photo on your card are completely independent of your friend's. Changing the address printed on your card does nothing to your friend's card.

That single idea — separate the template (what kind of thing this is, and what it can do) from the instances (the actual things, each carrying its own data) — is the entire foundation of object-oriented programming, usually shortened to OOP. In code, the template is called a class, and each individual instance built from it is called an object. This chapter is about learning to see programming problems this way: not as a list of steps to run, but as a collection of objects that hold data and know how to act on it.

The Problem OOP Actually Solves

To see why this separation matters, look at what happens without it. Suppose you are writing a small program for your school to store the name and marks of three students in section 9A, and to print each student's grade band. A first attempt, using plain variables, might look like this:

name1 = "Aditi"
marks1 = 88

name2 = "Rohan"
marks2 = 74

name3 = "Meera"
marks3 = 95

def grade(marks):
    if marks >= 90:
        return "A1"
    elif marks >= 75:
        return "A2"
    else:
        return "B1"

print(name1, grade(marks1))
print(name2, grade(marks2))
print(name3, grade(marks3))

Trace it: grade(88) checks 88 >= 90 (false), then 88 >= 75 (true), so it returns "A2". grade(74) fails both checks and falls to "B1". grade(95) passes the first check and returns "A1". The program prints:

Aditi A2
Rohan B1
Meera A1

This works for three students. Now imagine section 9A actually has 40 students. You would need 80 variables — name1 through name40 and marks1 through marks40 — and 40 nearly identical print lines. One typo, like writing grade(marks14) next to name17, silently prints the wrong grade for the wrong student, and nothing in the code stops that mistake.

A natural next attempt is to use two lists instead:

names = ["Aditi", "Rohan", "Meera"]
marks = [88, 74, 95]

for i in range(len(names)):
    print(names[i], grade(marks[i]))

This scales better, but it has a hidden fragility: names and marks are two completely separate lists that only line up because both were built in the same order, by hand, by you. If someone later sorts marks to find the topper, or deletes one name from names without also deleting the matching mark from marks, the two lists silently fall out of sync — index 0 in names no longer belongs to index 0 in marks. Nothing in the code enforces that a student's name and marks travel together. Data that belongs together is scattered across separate containers, held in sync only by the programmer's discipline. This is exactly the problem OOP removes: bundle a thing's data (its attributes) together with the code that acts on that data (its methods) into one unit, so they can never be pulled apart by accident.

Classes and Objects, Defined Precisely

A class is a blueprint that specifies two things: what pieces of data an object of this type will carry (its attributes) and what actions it can perform (its methods). A class holds no real data itself — it only describes the shape that data will take. An object (also called an instance) is one concrete thing built from that blueprint, carrying its own actual values for every attribute the class describes. The process of building an object from a class is called instantiation.

Return to the Aadhaar card: the printed layout — "there will be a name field, a number field, a photo, a QR code" — is the class. Your physical card, with "Aditi", a specific 12-digit number, and your own photo actually printed on it, is an object. The verification rule "scan the QR and check it matches the printed number" is a method — the same rule runs for every card, but it checks each card's own number.

Building a Student Class

Here is the same student-grading idea, rewritten as a class in Python:

class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks

    def grade(self):
        if self.marks >= 90:
            return "A1"
        elif self.marks >= 75:
            return "A2"
        else:
            return "B1"

__init__ is a special method called the constructor. Python runs it automatically every time a new object is built from this class, and it is where an object's attributes get their starting values. self is the parameter through which a method reaches the one specific object it was called on — think of it as the object saying "assign this data to me, not to some other student." self is not a special keyword; it is simply the conventional name every Python programmer uses for this first parameter.

Now three objects are created from this one class:

aditi = Student("Aditi", 88)
rohan = Student("Rohan", 74)
meera = Student("Meera", 95)

print(aditi.name, aditi.grade())
print(rohan.name, rohan.grade())
print(meera.name, meera.grade())

Trace the first line carefully, because everything else in this chapter depends on understanding this one step. Student("Aditi", 88) tells Python: create a brand-new, empty Student object in memory, then call __init__ on it, automatically passing that new object as self, and passing "Aditi" as name and 88 as marks. Inside __init__, self.name = name attaches the value "Aditi" to that specific new object under the label name; self.marks = marks attaches 88 to it the same way. The finished object — now carrying name = "Aditi" and marks = 88 — is handed back and stored in the variable aditi. The exact same sequence runs independently for rohan (with "Rohan", 74) and meera (with "Meera", 95): three separate objects, three separate blocks of memory, three independent sets of attribute values.

When aditi.grade() runs, Python automatically passes aditi itself as self inside grade. So self.marks resolves to aditi's own marks, 88: the check 88 >= 90 is false, 88 >= 75 is true, so it returns "A2". Calling rohan.grade() runs the identical code but with self bound to rohan, so self.marks is 74, giving "B1". The output is:

Aditi A2
Rohan B1
Meera A1

Notice something important: there is only one copy of the grade method's code, written once inside the class. It is not duplicated three times for three students. What is tripled is the data — three separate name/marks pairs, one per object — while the method code is shared and simply run once per object, each time bound to that object's own data through self. The diagram below makes this split concrete.

Diagram: One Class, Three Independent Objects

One Class, Three Independent Objects class Student: self.name self.marks def grade(self): ... (the blueprint — holds no data of its own) Student(name, marks) aditi : Student name = "Aditi" marks = 88 rohan : Student name = "Rohan" marks = 74 meera : Student name = "Meera" marks = 95 Each object stores its own name and marks — but all three share the same grade() code defined once in the class.

The indigo box on the left is the class — a description of what a Student object looks like, holding no actual student data itself. Each green box on the right is a separate object, built by calling Student("name", marks): it has its own real values for name and marks, completely independent of the other two objects. If you ran aditi.marks = 100 right now, only aditi's box would change — rohan and meera would be untouched, because their marks attributes live in separate memory, not inside the class.

Common Misconception: "The Class Stores the Data"

A very common mistake at this stage is to think that because class Student defines self.marks, there is one shared marks value that belongs to the class itself, and that every student secretly reads the same number. This is wrong, and the diagram above is the cure for it: class Student only writes down the instruction "every object of this type will have a marks attribute" — it is a rule about shape, not a container holding a number. The actual number 88, 74, or 95 only comes into existence when __init__ runs for a specific object and writes it onto that object, via self.marks = marks. Ask yourself: if the class itself held the marks, how could aditi.marks be 88 while rohan.marks is 74 at the very same moment? They can only differ because each object keeps its own copy — the class supplies only the rule for how that copy gets created and used.

A closely related mistake is treating self as pointless boilerplate that Python forces you to type. In fact self is the entire mechanism that lets one shared piece of method code apply correctly to many different objects. Without a way for grade to know which student's marks to check, the method could not distinguish aditi.grade() from rohan.grade() at all — both calls would run the exact same code, and that code needs a name for "the object this particular call is about." That name is self.

Objects Guard Their Own Data: Encapsulation

Bundling data with methods also gives objects a chance to protect their own data from invalid changes — an idea called encapsulation. Consider a simplified UPI-style wallet:

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

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

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient balance")
        else:
            self.balance = self.balance - amount
priya_wallet = Wallet("Priya", 500)
priya_wallet.deposit(200)
priya_wallet.withdraw(1000)
priya_wallet.withdraw(300)
print(priya_wallet.balance)

Trace it: the wallet starts at balance = 500. deposit(200) sets self.balance = 500 + 200 = 700. withdraw(1000) checks 1000 > 700, which is true, so it prints "Insufficient balance" and leaves balance at 700 — the withdrawal is refused. withdraw(300) checks 300 > 700, which is false, so self.balance becomes 700 - 300 = 400. The output is:

Insufficient balance
400

The important design choice is that nothing outside the Wallet class is allowed to touch balance except through deposit and withdraw. If a programmer instead wrote priya_wallet.balance = priya_wallet.balance - 1000 directly, skipping withdraw entirely, the wallet would happily go to -300, because that line never passes through the amount > self.balance check. Encapsulation is the discipline of changing an object's data only through its own methods, so the object itself — the one piece of code that understands its own rules — always gets a say before its data changes. This is exactly why real banking and UPI systems never let outside code directly overwrite an account balance; every change goes through a controlled operation that validates it first.

Objects Can Hold Other Objects

Once you can build one kind of object, you can build objects that hold collections of other objects — a natural next step in "thinking in objects." A classroom is naturally a collection of Student objects:

class Classroom:
    def __init__(self, section):
        self.section = section
        self.students = []

    def add_student(self, student):
        self.students.append(student)

    def average_marks(self):
        total = 0
        for s in self.students:
            total = total + s.marks
        return total / len(self.students)
room_9a = Classroom("9A")
room_9a.add_student(aditi)
room_9a.add_student(rohan)
room_9a.add_student(meera)
print(room_9a.average_marks())

Trace it: room_9a starts with an empty students list. Each add_student call appends one of the existing Student objects — the same aditi, rohan, and meera built earlier, not copies of them — so room_9a.students ends up holding all three. Inside average_marks, the loop adds 88 + 74 + 95 = 257, then divides by len(self.students), which is 3. Python prints the exact floating-point result:

85.66666666666667

This is a small example of composition: a Classroom object doesn't duplicate any student's name or marks — it simply keeps a list of the actual Student objects and asks each one for its own marks attribute when it needs it. If aditi's marks were updated later through her own object, room_9a.average_marks() would automatically reflect that change on its next call, because it reads live data from the same objects rather than a frozen copy.

Where This Leads

Two more OOP ideas build directly on what you have learned here, and you will meet them properly in later chapters: inheritance, where a new class such as GraduateStudent can be built by extending Student and reusing its attributes and methods instead of rewriting them from scratch; and polymorphism, where different classes can each define their own version of the same method name — a Teacher class and a Student class might each have their own grade method that behaves differently — and be used interchangeably by code that simply calls .grade() without needing to know which exact class it belongs to. Both ideas only make sense once "a class is a blueprint, an object is an independent instance carrying its own data" — the idea this chapter has been building — is completely solid.

Summary

  • A class is a blueprint describing what attributes (data) and methods (behavior) its objects will have. It holds no real data of its own.
  • An object (instance) is a specific thing built from a class, holding its own real values for every attribute. Instantiating a class multiple times produces multiple independent objects.
  • The constructor (__init__ in Python) runs automatically when an object is created and sets its starting attribute values.
  • self refers to the specific object a method was called on, letting one shared piece of method code correctly act on many different objects' data.
  • Encapsulation means an object's data should be changed only through its own methods, so the object can enforce its own rules (like refusing an overdraft).
  • Composition means one object can hold other objects (like a Classroom holding Student objects) and use their data without copying it.

Check Your Understanding

  1. Using the Student class exactly as defined above, a fourth object is created: neha = Student("Neha", 91). Trace neha.grade() step by step and state the exact returned value.
  2. A classmate says: "Since class Student defines self.marks, there's one shared marks value belonging to the whole class, used by every student." Using the earlier trace of aditi and rohan, explain exactly why this is incorrect.
  3. In the Wallet class, suppose withdraw were rewritten to just do self.balance = self.balance - amount, with no check at all. Describe the real-world problem this could cause, and name the OOP idea from this chapter that the original check was protecting.
  4. Add a method highest_scorer(self) to the Classroom class that returns the name of the student with the highest marks. Hint: loop through self.students the same way average_marks does, but keep track of the best Student object seen so far instead of a running total.
  5. True or False, with a one-line reason: when Python runs aditi.grade() and then rohan.grade(), it executes two separate copies of the grade method's code, one stored inside each object.

Answers to check yourself: (1) 91 >= 90 is true, so neha.grade() returns "A1". (3) An unchecked withdraw would let a wallet's balance go negative — effectively spending money that was never deposited; the missing safeguard was encapsulation, specifically the rule that data should only change through a method that validates the change first. (5) False — there is only one copy of grade's code, stored once inside the class; self simply binds that one shared piece of code to a different object's data on each call, which is exactly why aditi.grade() and rohan.grade() can return different results despite running identical code.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where object-oriented programming: thinking in objects is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting object-oriented programming: thinking in objects to at least 3 other topics you have studied.
Algorithms and Complexity: Why Speed Matters →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn