Object-oriented programming (OOP) is a programming paradigm that organises software around objects — entities that bundle related data (attributes) and behaviour (methods) — and models programs as networks of interacting objects, structured using the four principles of encapsulation, abstraction, inheritance, and polymorphism.
Object-oriented programming is modelling your program the same way you model the real world: as a collection of things that have properties and know how to do certain actions.
Objects: data and behaviour, together
A bank account has a balance and an owner. It can accept deposits, process withdrawals, and report its current balance. In procedural programming, the balance would be a variable, and the deposit and withdrawal logic would be separate functions. In OOP, all of that belongs together in one unit: an object.
A class is the blueprint. An object is an instance — a specific account created from that blueprint. The class BankAccount defines what every account has and can do. Each actual account (priya_account, rahul_account) is a separate object with its own data, but all use the same methods defined in the class.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # attribute: who owns this account
self.balance = balance # attribute: current balance
def deposit(self, amount): # method: behaviour
self.balance += amount
return self.balance
def withdraw(self, amount): # method: behaviour
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
# Two objects from the same class blueprint
priya = BankAccount("Priya", 1000)
rahul = BankAccount("Rahul", 500)
priya.deposit(200) # priya.balance is now 1200
rahul.withdraw(100) # rahul.balance is now 400
# priya and rahul are independent objects
Encapsulation: hiding what doesn't need to be seen
Encapsulation means bundling data and methods together, and controlling which parts of the code outside the object can see and modify the data. The balance of a bank account should only be changed through deposit() and withdraw() — not by any code that can directly set account.balance = 9999999.
Languages enforce this through access modifiers: private (Java, C++) hides a field from outside the class entirely. protected allows access from subclasses. Python uses a naming convention: _balance signals "don't touch this from outside" (a warning, not a hard restriction). Kotlin and Swift have internal, visible within the same module.
Inheritance: sharing and specialising
A SavingsAccount is a kind of BankAccount that also earns interest. Rather than rewriting all the account logic, SavingsAccount can inherit from BankAccount — it gets all the existing methods and attributes for free, and adds or overrides only what's different.
class SavingsAccount(BankAccount): # inherits from BankAccount
def __init__(self, owner, balance=0, interest_rate=0.04):
super().__init__(owner, balance) # call parent's __init__
self.interest_rate = interest_rate
def add_interest(self): # new method — not in BankAccount
interest = self.balance * self.interest_rate
self.deposit(interest) # reuses inherited deposit()
return interest
savings = SavingsAccount("Priya", 10000, 0.05)
savings.deposit(500) # inherited method works directly
savings.add_interest() # new method specific to SavingsAccount
Polymorphism: one interface, many forms
Polymorphism means the same operation can behave differently depending on what object it is applied to. If you have a list of different bank account types — checking, savings, fixed deposit — and you call account.get_interest() on each, each account type can implement that method differently. The caller doesn't need to know or care which specific type it's dealing with.
This is what makes OOP powerful for large systems: you can write code that works with a general type (any BankAccount) and it will automatically do the right thing for each specific subtype, without if/else chains checking the type.
Abstraction: simplifying complexity
Abstraction means exposing only what is necessary and hiding the implementation details. When you call priya.withdraw(200), you don't need to know how the withdrawal is implemented — whether it logs to a database, checks multiple conditions, or updates several internal fields. The method is your interface; what happens inside is hidden.
Abstraction lets systems grow. A bank system can have hundreds of account types; code that works with the abstract BankAccount interface works with all of them.
Where OOP came from
OOP originated with Simula 67 (Ole-Johan Dahl and Kristen Nygaard, Norwegian Computing Centre, 1967) — designed to simulate discrete events like ships entering a harbour. The insight: model the simulation entities directly as program objects. Alan Kay extended these ideas at Xerox PARC in the early 1970s, creating Smalltalk — the first purely OOP language. Kay coined the term "object-oriented." C++ (Bjarne Stroustrup, 1979) brought OOP to systems programming. Java (Sun Microsystems, 1995) made OOP the dominant enterprise paradigm.
The four principles in depth
Encapsulation
Encapsulation serves two purposes: data hiding (preventing invalid state by controlling how data is modified) and information hiding (reducing coupling between components by hiding implementation details). These are related but distinct. Data hiding prevents bugs; information hiding enables change — if clients only access an object through its public interface, the implementation can be completely rewritten without breaking the clients.
Inheritance and the Liskov Substitution Principle
Inheritance creates an is-a relationship: a SavingsAccount is a BankAccount. The Liskov Substitution Principle (Barbara Liskov, 1987, formalised 1994) states: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any desirable properties of the program. In practice: a subclass must honour all the contracts of its superclass — it can extend behaviour, but must not violate it.
Violation example: if BankAccount.withdraw() guarantees the balance never goes negative, and OverdraftAccount overrides it to allow negative balances, code that assumes balances are non-negative will break when given an OverdraftAccount. This is a LSP violation.
Polymorphism: subtype vs. parametric
Subtype polymorphism (runtime polymorphism) is the classic OOP kind: a method call on a base class reference dispatches to the correct subclass method at runtime, via a vtable (virtual function table) in C++ or equivalent mechanism. This is what virtual methods in C++ and all method overriding in Java achieve.
Parametric polymorphism (generics) is a different dimension: a single piece of code works for any type. Java generics (List<T>), C++ templates, Rust generics, and TypeScript generics are all parametric polymorphism. Not specific to OOP — functional languages use it extensively.
Interfaces and abstract classes
Abstraction is concretely implemented through interfaces (in Java, Go, TypeScript) and abstract classes. An interface declares what methods a type must have, without specifying how. Any class that implements the interface can be used wherever the interface is expected. Go's interfaces are structural (implicit) — any type with the right methods satisfies the interface automatically, without declaring it.
interface Account {
void deposit(double amount);
double withdraw(double amount);
double getBalance();
}
class SavingsAccount implements Account {
private double balance;
public void deposit(double amount) { balance += amount; }
public double withdraw(double amount) {
if (amount > balance) throw new IllegalArgumentException();
balance -= amount; return balance;
}
public double getBalance() { return balance; }
}
// This method works with ANY Account — polymorphism
void processAccounts(List<Account> accounts) {
for (Account account : accounts) {
account.deposit(100); // correct method called per type
}
}
Composition over inheritance
Deep inheritance hierarchies are a recognised design problem — they create tight coupling, make code hard to follow, and violate the Liskov principle when inheritance is used for code reuse rather than genuine is-a relationships. The Gang of Four design patterns book (Gamma et al., 1994) explicitly advises: "favour object composition over class inheritance." Composition uses has-a relationships: a Car has an Engine rather than inheriting from Engine. The engine can be swapped; the car delegates engine-related behaviour to its engine object. This produces more flexible, maintainable designs.
Prototype-based OOP
JavaScript does not use classes in the same way Java does. JavaScript's original OOP model is prototype-based: objects inherit directly from other objects via a prototype chain, without a separate class structure. class syntax in JavaScript (introduced in ES6 / ECMAScript 2015) is syntactic sugar over the prototype chain — the underlying mechanism is unchanged. This matters when debugging prototype chain lookups or understanding how this binding works.
private keyword. The convention _name signals "don't use this outside the class" but cannot prevent it. __name (name mangling) makes external access harder but not impossible. Python relies on convention and programmer discipline for encapsulation, not language enforcement.The origin of OOP: Simula and Smalltalk
Simula 67 (Dahl & Nygaard, 1967) introduced classes, objects, inheritance, and virtual procedures. It was designed as a simulation language — entities in a simulation (ships, queues, processes) mapped directly to objects in the program. This physical modelling intuition remains the best way to identify good OOP boundaries: if an entity in the problem domain has both state and behaviour, it is a candidate for an object.
Alan Kay's Smalltalk (Xerox PARC, 1972–1980) pursued a different vision: extreme uniformity and message passing. In Smalltalk, everything — including integers and booleans — is an object. Sending a message to an object (5 + 3 sends the + message to the object 5) is the only way to cause any effect. Late binding — method dispatch resolved at runtime, never at compile time — was fundamental. Kay later said the most important idea in Smalltalk was message passing, not the object system itself.
Vtables and dynamic dispatch
In C++, virtual method calls are implemented via a vtable (virtual function table). Each class with virtual methods has a vtable — an array of function pointers, one per virtual method. Each object has a hidden pointer to its class's vtable. A virtual method call dereferences the vtable pointer and calls through the appropriate function pointer — two pointer dereferences and an indirect call, compared to a direct call for non-virtual methods. This is the cost of runtime polymorphism in C++: ~1–3 ns on modern hardware, often negligible, but a concern in inner loops of high-performance code.
Java uses a similar mechanism via the Method Table (mtable) in the JVM. Python's method dispatch goes through the __mro__ (Method Resolution Order) — a linearisation of the class hierarchy computed via the C3 algorithm (Barrett et al., 1996) — which handles multiple inheritance in a consistent, predictable way.
Liskov, B. & Wing, J. (1994). "A behavioral notion of subtyping." ACM TOPLAS, 16(6), 1811–1841. — The formal statement of the Liskov Substitution Principle. Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — The definitive OOP design patterns reference. Kay, A. C. (1993). "The Early History of Smalltalk." ACM SIGPLAN Notices, 28(3), 69–95. — Primary source for the origin and intent of OOP.