FLASH SALE
00:00:00
$49$9960% OFF
Back to Blog
OOP Object Oriented Programming Software Design Interview Prep Python

OOP Concepts Every Developer Should Master in 2026

Vishnu Darshan Sanku
February 14, 2026
OOP Concepts Every Developer Should Master in 2026

OOP Concepts Every Developer Should Master in 2026

Here’s a confession: I used to think I understood OOP because I could define a class and create objects. Then I tried to design a chess game in an interview and realized I had no idea how to model pieces, boards, moves, and rules as a system of interacting objects.

Knowing the syntax of OOP isn’t the same as thinking in objects. And in 2026, with LLD interviews becoming standard at every major tech company, the gap between “I can write a class” and “I can design a system” has never mattered more.

This guide covers the OOP concepts that actually matter—not textbook definitions, but the practical skills you need to build clean, extensible software and ace design interviews.

Who This Is For

This guide is for developers who know the basics of at least one OOP language (Python, Java, TypeScript, C++) but want to go deeper. If you’ve never written a class before, start with our Classes and Objects refresher first.

The 4 Pillars of OOP (With Real Analogies)

You’ve seen these four words a thousand times. Let’s make them stick—not with textbook definitions, but with mental models you’ll actually remember.

1. Encapsulation: The ATM Machine

Pillar 1 Data Protection

The analogy: An ATM lets you withdraw money, check your balance, and deposit cash. But you can’t reach inside and grab bills directly. You interact through a defined interface (the screen and buttons), and the internal mechanism is hidden.

In code terms: Encapsulation bundles data and the methods that operate on that data into a single unit (a class), and restricts direct access to the internal state.

class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self._owner = owner # protected
self.__balance = balance # private
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit must be positive")
self.__balance += amount
def withdraw(self, amount: float) -> None:
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
@property
def balance(self) -> float:
return self.__balance
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500) # ✅ Uses the controlled interface
account.withdraw(200) # ✅ Validates before acting
print(account.balance) # ✅ Read-only access via property
# account.__balance = 999 # ❌ Can't manipulate directly

Why it matters: Without encapsulation, any piece of code can modify any data, creating bugs that are impossible to trace. Encapsulation creates trust boundaries—you know that balance can only change through deposit() and withdraw(), both of which validate the operation first.

The most common mistake: Making everything public “for convenience.” If every field is public, you have a struct, not an object. The moment you need to add validation or logging, you’ll have to find and update every place that directly modifies the field.

Deep dive: Encapsulation


2. Abstraction: The Car Dashboard

Pillar 2 Hiding Complexity

The analogy: When you drive a car, you use the steering wheel, pedals, and gear shift. You don’t need to understand how the engine combustion cycle works, how the transmission converts gear ratios, or how the anti-lock braking system prevents wheel lockup. The car abstracts the complexity behind a simple interface.

In code terms: Abstraction means exposing only what’s necessary and hiding the implementation details. In Python, this is typically done with abstract base classes:

from abc import ABC, abstractmethod
class NotificationSender(ABC):
"""Users of this class only need to know about send().
They don't care HOW the notification is delivered."""
@abstractmethod
def send(self, recipient: str, message: str) -> bool:
pass
class EmailSender(NotificationSender):
def send(self, recipient: str, message: str) -> bool:
# Complex SMTP logic hidden here:
# connect to server, authenticate, format MIME,
# handle attachments, retry on failure...
return self._send_via_smtp(recipient, message)
def _send_via_smtp(self, to: str, msg: str) -> bool:
# 50 lines of SMTP details nobody else needs to see
...
return True
class SMSSender(NotificationSender):
def send(self, recipient: str, message: str) -> bool:
# Twilio API integration hidden here
return self._send_via_twilio(recipient, message)
def _send_via_twilio(self, to: str, msg: str) -> bool:
...
return True

Why it matters: Abstraction lets you think at the right level. When designing a notification service, you think about “send a notification”—not “connect to an SMTP server on port 587, authenticate with TLS, format a MIME multipart message…” Abstraction manages the cognitive load of complex systems.

Deep dive: Abstraction


Encapsulation vs Abstraction: The Most Confused Pair

These two get mixed up constantly. Here’s the clearest distinction:

Encapsulation

  • Protects internal data from outside access
  • Mechanism: Access modifiers (private, protected, public)
  • Focus: “Don’t touch my internals”
  • Example: Making __balance private so no one can modify it directly
  • Level: Implementation detail within a class

Abstraction

  • Hides complexity behind a simple interface
  • Mechanism: Abstract classes, interfaces, APIs
  • Focus: “You don’t need to know how this works”
  • Example: A send() method that hides SMTP vs Twilio internals
  • Level: Design decision across classes

The one-liner: Encapsulation is about protecting data. Abstraction is about hiding complexity. A class can (and usually should) use both.


3. Inheritance: The Shape Hierarchy

Pillar 3 Code Reuse

The analogy: In biology, a German Shepherd inherits traits from the broader Dog category, which inherits from Mammal, which inherits from Animal. Each level adds specialization while keeping the general traits.

In code terms: Inheritance lets a class (child) derive from another class (parent), inheriting its attributes and methods while adding or overriding its own:

class Shape:
def __init__(self, color: str):
self.color = color
def area(self) -> float:
raise NotImplementedError
def describe(self) -> str:
return f"A {self.color} {self.__class__.__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, color: str, radius: float):
super().__init__(color)
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, color: str, width: float, height: float):
super().__init__(color)
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
# Both work as Shape
shapes: list[Shape] = [Circle("red", 5), Rectangle("blue", 4, 6)]
for shape in shapes:
print(shape.describe())
# "A red Circle with area 78.54"
# "A blue Rectangle with area 24.00"

Why it matters: Inheritance enables polymorphism (Pillar 4), allows code reuse, and models “is-a” relationships naturally. A Circle is a Shape. A SavingsAccount is a BankAccount.

The danger: Inheritance is the most overused OOP feature. Deep hierarchies (5+ levels) become rigid and hard to change. A common rule of thumb: if you’re inheriting just for code reuse, you’re doing it wrong. That’s what composition is for.

Deep dive: Inheritance


4. Polymorphism: The Universal Remote

Pillar 4 Flexible Interfaces

The analogy: A universal remote has a “power” button that works with your TV, sound bar, and streaming box. The same button, different devices, different behavior. The remote doesn’t need to know how each device turns on—it just sends the signal.

In code terms: Polymorphism means “many forms”—the same interface (method name) behaves differently based on the object’s type:

class PaymentMethod:
def pay(self, amount: float) -> str:
raise NotImplementedError
class CreditCard(PaymentMethod):
def pay(self, amount: float) -> str:
return f"Charged ${amount:.2f} to credit card"
class UPI(PaymentMethod):
def pay(self, amount: float) -> str:
return f"Transferred ${amount:.2f} via UPI"
class Crypto(PaymentMethod):
def pay(self, amount: float) -> str:
return f"Sent {amount / 50000:.6f} BTC"
def checkout(method: PaymentMethod, amount: float):
# This function doesn't know or care which payment type it is
print(method.pay(amount))
# All work through the same interface
checkout(CreditCard(), 99.99) # "Charged $99.99 to credit card"
checkout(UPI(), 99.99) # "Transferred $99.99 via UPI"
checkout(Crypto(), 99.99) # "Sent 0.002000 BTC"

Why it matters: Polymorphism is the foundation of the Strategy pattern—the single most used pattern in LLD interviews. Every time you see “swap algorithms at runtime” or “support multiple types,” you’re using polymorphism.

Python’s superpower—Duck Typing: In Python, you don’t even need inheritance for polymorphism. If an object has a pay() method, it works—regardless of its class hierarchy. “If it walks like a duck and quacks like a duck, it’s a duck.”

class Cash: # No inheritance!
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} in cash"
checkout(Cash(), 99.99) # Works perfectly in Python

Deep dive: Polymorphism


Class Relationships: The Part Most Developers Skip

Knowing the 4 pillars is necessary but not sufficient. The real skill is knowing how classes connect to each other. This is where most LLD designs succeed or fail.

Association: “Uses” or “Knows About”

A Doctor treats a Patient. They interact but exist independently. Neither owns the other.

class Doctor:
def __init__(self, name: str):
self.name = name
def treat(self, patient: "Patient") -> str:
return f"Dr. {self.name} treats {patient.name}"
class Patient:
def __init__(self, name: str):
self.name = name

The doctor and patient have their own lifecycles. Deleting the doctor doesn’t delete the patient. This is the loosest relationship.

Deep dive: Association

Aggregation: “Has, But Doesn’t Own”

A Department has Employees, but employees exist even if the department is dissolved. The department doesn’t control their lifecycle.

class Department:
def __init__(self, name: str):
self.name = name
self.employees: list[Employee] = []
def add_employee(self, employee: "Employee"):
self.employees.append(employee)
# Employees exist independently
alice = Employee("Alice")
bob = Employee("Bob")
engineering = Department("Engineering")
engineering.add_employee(alice)
engineering.add_employee(bob)
# Delete the department — Alice and Bob still exist

Deep dive: Aggregation

Composition: “Owns and Controls”

A House has Rooms. Destroy the house, and the rooms are gone. The house controls the lifecycle of its rooms.

class House:
def __init__(self, address: str):
self.address = address
# Rooms are created BY the house — they can't exist alone
self.rooms = [
Room("Kitchen"),
Room("Bedroom"),
Room("Bathroom"),
]
class Room:
def __init__(self, name: str):
self.name = name

This is the strongest “has-a” relationship and the basis of the Composition over Inheritance principle.

Deep dive: Composition

Dependency: “Temporarily Uses”

A ReportGenerator uses a Printer to print, but doesn’t hold a reference to it. The printer is passed in when needed and then forgotten.

class ReportGenerator:
def generate_and_print(self, data: dict, printer: "Printer"):
report = self._format(data)
printer.print(report) # Uses printer, doesn't own it

This is the weakest relationship—the dependency exists only for the duration of the method call.

Deep dive: Dependency


Inheritance vs Composition: The Debate That Actually Matters

This is the single most important design decision you’ll make in any LLD problem. Get it wrong and your code becomes rigid, fragile, and hard to extend.

When to Use Inheritance

Inheritance is right when there’s a genuine “is-a” relationship and the child class truly represents a specialization of the parent:

# ✅ Good: A SavingsAccount IS a BankAccount
class BankAccount:
def deposit(self, amount): ...
def withdraw(self, amount): ...
class SavingsAccount(BankAccount):
def add_interest(self): ...
# ✅ Good: Each chess piece IS a Piece
class Piece:
def can_move(self, board, start, end) -> bool: ...
class Knight(Piece):
def can_move(self, board, start, end) -> bool:
# L-shaped movement logic
...

When to Use Composition

Composition is right when classes have a “has-a” relationship, or when you want to combine behaviors flexibly:

# ✅ Good: A Car HAS an Engine (not "is an" Engine)
class Car:
def __init__(self, engine: Engine, transmission: Transmission):
self.engine = engine
self.transmission = transmission
# Now we can mix and match:
electric_auto = Car(ElectricEngine(), AutomaticTransmission())
gas_manual = Car(GasEngine(), ManualTransmission())
hybrid_auto = Car(HybridEngine(), AutomaticTransmission())

The Inheritance Trap

Watch what happens when you try to model the same thing with inheritance:

# ❌ Bad: Inheritance explosion
class ElectricAutomaticCar(Car): ...
class ElectricManualCar(Car): ...
class GasAutomaticCar(Car): ...
class GasManualCar(Car): ...
class HybridAutomaticCar(Car): ...
class HybridManualCar(Car): ...
# 3 engines × 2 transmissions = 6 classes. Add 4WD? Now 12 classes.

With composition, adding a new engine type means one new class. With inheritance, it means N new classes for every combination.

The Rule of Thumb

Default to composition. Only use inheritance when:

  1. There’s a clear “is-a” relationship
  2. The child genuinely specializes the parent’s behavior
  3. You need polymorphism through a shared base type

If you’re inheriting just to reuse code, composition is almost always better.


Enums: The Unsung Hero of Clean Design

Enums are criminally underused. Every time you see a string like "active", "pending", or "cancelled" hardcoded in your codebase, that’s a place where an enum would prevent bugs.

from enum import Enum
# ❌ Without enums: typos cause silent bugs
def update_order(order_id: int, status: str):
...
update_order(123, "actve") # Typo — no error, just wrong data
# ✅ With enums: type safety catches mistakes
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
PREPARING = "preparing"
DELIVERED = "delivered"
CANCELLED = "cancelled"
def update_order(order_id: int, status: OrderStatus):
...
update_order(123, OrderStatus.CONFIRMED) # ✅ Type-safe
update_order(123, OrderStatus.ACTVE) # ❌ AttributeError — caught immediately

Enums appear in almost every LLD problem: vehicle types in a parking lot, piece types in chess, machine states in a vending machine, order statuses in a restaurant system.

Deep dive: Enums


UML: The Language of LLD Interviews

You don’t need to be a UML expert, but you need to read and draw basic class diagrams. They’re the communication tool of LLD interviews.

Here’s a quick reference for the notation that matters:

The key symbols:

SymbolRelationshipMeaningExample
◁—— (solid line, hollow arrow)Inheritance”is-a”Dog is an Animal
◆—— (solid diamond)Composition”owns” (lifecycle dependent)Dog owns Collar
◇—— (hollow diamond)Aggregation”has” (independent lifecycle)Owner has Animals
- - -> (dashed arrow)Dependency”uses temporarily”Method parameter

Visibility markers: + public, - private, # protected

In LLD interviews, you’ll draw these on a whiteboard or describe them verbally. Being fluent in this notation shows you think at the design level, not just the code level.

Deep dive: UML Class Diagrams


Interfaces: Contracts That Keep Code Flexible

Interfaces (or abstract base classes in Python) define what a class must do without specifying how. They’re the backbone of extensible design and appear in every well-designed LLD.

from abc import ABC, abstractmethod
class SearchStrategy(ABC):
@abstractmethod
def search(self, catalog: list, query: str) -> list:
pass
class TitleSearch(SearchStrategy):
def search(self, catalog: list, query: str) -> list:
return [book for book in catalog if query.lower() in book.title.lower()]
class AuthorSearch(SearchStrategy):
def search(self, catalog: list, query: str) -> list:
return [book for book in catalog if query.lower() in book.author.lower()]
class ISBNSearch(SearchStrategy):
def search(self, catalog: list, query: str) -> list:
return [book for book in catalog if book.isbn == query]

Why interfaces matter:

  1. Testability: Mock the interface in unit tests instead of using the real implementation
  2. Extensibility: Add new strategies without modifying existing code (Open/Closed Principle)
  3. Decoupling: The caller depends on the interface, not the concrete class (Dependency Inversion)

Deep dive: Interfaces


How OOP Shows Up in LLD Interviews

Let’s connect the theory to practice. Here’s how OOP concepts map to actual interview problems:

OOP ConceptInterview ApplicationExample Problem
EncapsulationHiding internal state behind methodsATM System — balance only changes through validated transactions
AbstractionAbstract base classes for strategiesRate Limiter — abstract RateLimiter with concrete TokenBucket, SlidingWindow
InheritanceClass hierarchies for entity typesChess GamePieceKing, Queen, Rook, etc.
PolymorphismStrategy pattern, interchangeable behaviorsElevator System — dispatch strategies
CompositionBuilding complex objects from simpler onesParking LotParkingLot composed of Floors composed of Spots
EnumsState machines, type safetyVending MachineMachineState.IDLE, MachineState.DISPENSING
InterfacesDependency inversion, testabilityNotification ServiceNotificationSender interface

The Pattern

Almost every LLD problem follows this structure:

  1. Identify entities (classes) from requirements
  2. Define relationships between them (composition, association)
  3. Use enums for state and types
  4. Create interfaces for interchangeable behaviors
  5. Apply polymorphism through the Strategy or State pattern

Master these OOP building blocks and the patterns fall into place naturally.


Common OOP Interview Questions

Beyond design problems, you might get conceptual OOP questions. Here are the ones that come up most:

Abstract class: Can have both abstract (unimplemented) and concrete (implemented) methods. Can have state (instance variables). A class can only extend one abstract class (in most languages).

Interface: Only defines method signatures (no implementation, though Python and Java 8+ allow defaults). A class can implement multiple interfaces.

When to use which:

  • Abstract class when you have shared implementation (e.g., a Vehicle base class with a common start() method)
  • Interface when you want a pure contract (e.g., Searchable, Sortable, Serializable)

In Python, both are implemented using ABC, so the distinction is more about intent than syntax.


Key Takeaways

Remember These

  1. Encapsulation protects, abstraction hides — know the difference
  2. Default to composition over inheritance — it’s more flexible
  3. Use enums for anything with a fixed set of values — states, types, categories
  4. Interfaces enable extensibility — they’re the foundation of Strategy, Observer, and Factory patterns
  5. Learn UML basics — you need to communicate designs visually in interviews
  6. Class relationships matter as much as classes — association, aggregation, composition, dependency
  7. OOP is a means, not an end — the goal is clean, maintainable, extensible code

“The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.” — Joe Armstrong

OOP is a powerful tool, but like any tool, its value comes from knowing when and how to use it. Master the concepts in this guide, and you’ll have the foundation to design clean systems—not just write classes.