The problem OOP solves
So far, data (variables) and behavior (functions) live separately. Model a bank account that way:
balance = 1000
owner = "Asha"
def deposit(balance, amount):
return balance + amount
Now model two accounts. Now ten thousand. Now make sure no code ever sets a balance negative, and that every withdrawal checks the limit… With loose variables and functions, nothing enforces that the right data and the right rules travel together.
Object-oriented programming (OOP) fixes this by bundling data and the functions that operate on it into one unit: an object.
Classes and objects
A class is a blueprint; an object (or instance) is one concrete thing built from it. Cookie cutter vs cookies. Architectural plan vs actual houses.
# Python
class BankAccount:
def __init__(self, owner, balance): # constructor: runs at creation
self.owner = owner # attributes: the object's data
self.balance = balance
def deposit(self, amount): # method: a function on the object
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds"
self.balance -= amount
return "OK"
asha = BankAccount("Asha", 1000) # one object...
rahul = BankAccount("Rahul", 500) # ...another, fully independent
asha.deposit(250)
print(asha.balance) # 1250
print(rahul.balance) # 500 — untouched
// Java
public class BankAccount {
private String owner; // "private": only this class can touch these
private double balance;
public BankAccount(String owner, double balance) { // constructor
this.owner = owner;
this.balance = balance;
}
public void deposit(double amount) {
this.balance += amount;
}
public double getBalance() {
return balance;
}
}
BankAccount asha = new BankAccount("Asha", 1000);
asha.deposit(250);
// C++
class BankAccount {
private:
std::string owner;
double balance;
public:
BankAccount(std::string owner, double balance)
: owner(owner), balance(balance) {}
void deposit(double amount) { balance += amount; }
double getBalance() const { return balance; }
};
BankAccount asha("Asha", 1000);
asha.deposit(250);
Vocabulary:
- Constructor — the special function that runs when an object is created
(
__init__/ class-named method), setting up its starting data. - Attribute / field — a variable living inside an object (
balance). - Method — a function living inside a class, acting on a specific object.
self/this— the method's handle on which object it was called on.asha.deposit(250)means "run deposit with self = asha."
You've been using objects all along: in Python, "hello".upper() is a
method call on a string object; a list is an object whose methods include
append. OOP just lets you define your own kinds.
The four pillars
Interviewers love asking for these by name; here they are at Level-1 depth (Level 5's OOP & SOLID goes to interview depth).
1. Encapsulation — data with a bodyguard
Bundle data with its rules, and hide the raw data so the rules can't be
bypassed. In the Java/C++ versions, balance is private: outside code
cannot do asha.balance = -5000; it must go through deposit/withdraw,
which enforce the rules. The object exposes a small public surface — exactly
the interface idea again, at the scale of one
object. (Python signals "keep out" by underscore convention, _balance,
trusting you; Java/C++ bring a compiler-enforced bodyguard.)
2. Abstraction — usable without understanding
You call car.start(); you don't manage fuel injection. A well-designed
class is operated through simple methods that hide genuinely complex
machinery — file.read(), model.predict(image). Abstraction is why
encapsulation is worth it: hide the internals, and users think at your
interface's level, not your implementation's.
3. Inheritance — "is a special kind of"
A class can extend another, inheriting its data and methods and adding its own:
class SavingsAccount(BankAccount): # SavingsAccount IS A BankAccount
def __init__(self, owner, balance, rate):
super().__init__(owner, balance) # run the parent's constructor
self.rate = rate
def add_interest(self): # new ability
self.deposit(self.balance * self.rate)
SavingsAccount gets deposit and withdraw for free. Use inheritance
only for true "is-a" relationships — a savings account is an account; a
car has an engine (that's composition: put an Engine object inside the
Car). Beginners over-inherit; the industry preference is "composition over
inheritance," a phrase you'll meet again in Level 5.
4. Polymorphism — same call, different behavior
Code that works on the parent type automatically works on every child, each responding in its own way:
class Cat(Animal):
def speak(self): return "Meow"
class Dog(Animal):
def speak(self): return "Woof"
for pet in [Cat(), Dog(), Cat()]:
print(pet.speak()) # Meow / Woof / Meow — one line, many behaviors
The loop never asks "what kind are you?" — no if/elif chain over types. Add a
Parrot class tomorrow and this loop handles it unchanged. That
open-for-extension quality is the heart of the design patterns in Level 5.
(This diagram style is UML — the standard sketch language for class design, and the whiteboard language of Level 5 interviews.)
Python Conventions: Underscores & Dunder Methods
Python objects are highly dynamic, and the language relies on specific variable naming conventions and special methods (called "dunders", short for double underscore) to manage object behavior.
1. The Underscore Conventions (_ vs __)
- Single Underscore (
_variable): A convention indicating that a variable or method is intended for internal use only (protected/private). Python does not prevent outside access, but developers respect it as a "keep out" sign. - Double Underscores (
__variable): Triggers name mangling, where Python automatically renames the variable to_ClassName__variableto prevent accidental overriding in child classes.
class Node:
def __init__(self, val):
self.val = val
self._internal_id = 123 # internal use only (protected convention)
self.__private_key = "abc" # name mangled (private)
n = Node(5)
print(n._internal_id) # 123 (works, but frowned upon)
# print(n.__private_key) # ❌ Raises AttributeError (name is mangled!)
print(n._Node__private_key) # "abc" (how to access the mangled name)
2. Dunder Methods (Magic Methods)
Dunder methods are special pre-defined methods that start and end with double underscores. They allow your custom classes to integrate with Python's built-in syntax (operators, functions):
__str__and__repr__(Print representation): Determines what is displayed when you print an object or view it in a list. In DSA, defining__repr__makes debugging custom node structures (trees, lists) incredibly easy.__len__(Sizing): Allows callinglen(obj)on your custom collection.__lt__(Less Than): Defines the<comparison logic. This is critical in DSA when pushing custom objects into a min-heap or priority queue usingheapq.
import heapq
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
# Debug representation
def __repr__(self):
return f"{self.name}({self.score})"
# Less-than comparison (compares students by score)
def __lt__(self, other):
return self.score < other.score
s1 = Student("Asha", 91)
s2 = Student("Rahul", 84)
print(s1) # Prints: Asha(91) - uses __repr__
# Heap queue uses < under the hood to organize elements
heap = []
heapq.heappush(heap, s1)
heapq.heappush(heap, s2)
# Pops student with smallest score (Rahul) because we defined __lt__
print(heapq.heappop(heap)) # Prints: Rahul(84)
Industry perspective
- Java is OOP-mandatory (all code lives in classes); Python and C++ are multi-paradigm — you choose when objects help. Knowing when is the skill: a 10-line script needs no classes; a banking system with 400 entity types can't live without them.
- Every major framework hands you its power through classes you extend or objects you configure — Android Activities, Spring services (Level 7), React components historically (Level 8).
- LLD interviews (Level 5) are OOP interviews: "design a parking lot" means "show me your classes, their responsibilities and relationships." What you learned here — plus SOLID and patterns — is exactly that round.
Common beginner mistakes
- Classes for everything. A function returning a value doesn't need a
Calculatorclass. Reach for a class when data and its rules belong together and you'll need several of them. - The God object — one
Managerclass holding all the data and methods. That's procedural code in an OOP costume. Many small classes, one responsibility each. - Public everything — exposing every field defeats encapsulation: any code can now break your invariants, and you can never change internals without breaking users.
- Deep inheritance towers —
A > B > C > D > Ewhere a change at the top breaks everything below. Prefer shallow trees and composition. - Forgetting objects are references —
account2 = account1aliases one object (as with lists); both names see every change.
Think it through
The pillar interviewers probe hardest is polymorphism — because it kills the if/elif-over-types code beginners write by reflex. Reason through the payoff before revealing.
PROBLEMYou have a list of mixed shapes (circles, rectangles, …) and must sum their areas. A new shape type is added every month. Design it so adding a shape never forces you to edit the summing code.
- 1
The naive approach
“If each shape just stored a 'kind' string, how would total_area look — and what breaks?”
- 2
The OOP insight
“What if each shape knew how to compute its own area?”
unlocks after the stage above - 3
Give them a common type
“How do the shapes agree to share the area() interface?”
unlocks after the stage above - 4
Code it
“Base with area(), two subclasses, and a sum loop that asks no questions.”
unlocks after the stage above - 5
The payoff
“A Triangle ships next month. What changes?”
unlocks after the stage above
Check yourself
1. Encapsulation means:
2. Polymorphism lets a loop call pet.speak() over cats and dogs with no if/elif over types. The main benefit is:
3. When should you use inheritance rather than composition?
4. `account2 = account1`, where account1 is an object. What's true?
Interview perspective
Practice
Beginner
- Write a
Rectangleclass: constructor takes width and height; methodsarea()andperimeter(). Create three rectangles and print their areas. - Add an
is_square()method returning a boolean. Then explain: why is this better as a method than as a free function taking width and height?
Intermediate
- Build a
Librarysystem: aBookclass (title, author, checked_out) and aLibraryclass holding a list of books with methodsadd_book,checkout(title),return_book(title). Enforce: a checked-out book can't be checked out again. - Make
EBookinherit fromBookwith afile_sizeattribute, and override itsdescribe()method. Loop over a mixed list printing descriptions — polymorphism in action.
Advanced
- Write the
BankAccountin Java or C++ with truly private balance, and try to set it directly from outside — read the compiler error carefully. Then add atransfer(other, amount)method that's safe even when funds are insufficient. Which earlier concept (transactions, from Level 0) is this a tiny version of?
This completes the Level 1 starter set. Exceptions, collections and file handling land next in this section — then Level 2's data structures await. For the interview-grade version of today's ideas, see OOP & SOLID.