Object-oriented programming (OOP) is the foundation of most modern software development. Whether you're interviewing for a junior role or a senior engineering position, OOP questions test your grasp of fundamental design principles, language mechanics, and real-world application. This guide covers the 50 most common OOP interview questions with detailed answers and code examples.
OOP at a Glance
| Pillar | What it means | Example |
|---|---|---|
| Encapsulation | Bundle data + behavior; hide internals | Private fields + public getters |
| Abstraction | Expose what's needed, hide complexity | Interface / abstract class |
| Inheritance | Reuse behavior from parent class | Dog extends Animal |
| Polymorphism | Same interface, different behavior | speak() differs per animal |
Core OOP Concepts
1. What are the four pillars of OOP?
The four pillars are Encapsulation, Abstraction, Inheritance, and Polymorphism (EAIP).
- Encapsulation — Binding data (attributes) and methods together while restricting direct access to internal state. Protects data integrity.
- Abstraction — Hiding implementation complexity and exposing only what the caller needs to know.
- Inheritance — A child class acquires properties and behaviors of a parent class, enabling code reuse.
- Polymorphism — Objects of different types can be treated through a common interface; the right method is called at runtime.
2. What is encapsulation? Why is it important?
Encapsulation restricts access to an object's internal state and exposes a controlled interface.
class BankAccount:
def __init__(self, balance: float):
self.__balance = balance # private
def deposit(self, amount: float):
if amount > 0:
self.__balance += amount
def get_balance(self) -> float:
return self.__balance
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance()) # 1500
# account.__balance = -999 # AttributeError — can't access directly
Why it matters:
- Prevents invalid state (e.g., negative balance set directly)
- Allows internal implementation to change without breaking callers
- Easier debugging — state only changes through known methods
3. What is abstraction? Difference from encapsulation?
Abstraction hides what the implementation does (complexity). Encapsulation hides how data is stored (internals).
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: # abstraction — callers just call area()
pass
class Circle(Shape):
def __init__(self, radius: float):
self.__radius = radius # encapsulation — hides storage
def area(self) -> float:
return 3.14159 * self.__radius ** 2
| Abstraction | Encapsulation | |
|---|---|---|
| Focus | Design level — what to expose | Implementation level — how to hide |
| Tool | Abstract class, interface | Private/protected fields |
| Goal | Reduce complexity | Protect data integrity |
4. What is inheritance? Types of inheritance?
Inheritance allows a class (child/subclass) to inherit attributes and methods from another class (parent/superclass).
class Animal {
protected String name;
Animal(String name) { this.name = name; }
void speak() { System.out.println(name + " makes a sound"); }
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override
void speak() { System.out.println(name + " barks"); }
}
Types:
| Type | Description | Support |
|---|---|---|
| Single | One parent, one child | All OOP languages |
| Multilevel | A→B→C chain | All OOP languages |
| Hierarchical | One parent, multiple children | All OOP languages |
| Multiple | One child, multiple parents | C++, Python (not Java/C#) |
| Hybrid | Mix of above | C++, Python |
Java/C# avoid multiple inheritance of classes to prevent the diamond problem — use interfaces instead.
5. What is polymorphism? Compile-time vs runtime?
Polymorphism = "many forms" — same method name behaves differently based on object type.
Compile-time (static) polymorphism — Method Overloading:
class MathUtils {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // different params
int add(int a, int b, int c) { return a + b + c; }
}
Runtime (dynamic) polymorphism — Method Overriding:
Animal animal = new Dog("Rex"); // Dog reference in Animal variable
animal.speak(); // calls Dog.speak() — determined at runtime
| Compile-time | Runtime | |
|---|---|---|
| Achieved by | Method overloading | Method overriding |
| Resolved at | Compile time | Runtime (dynamic dispatch) |
| Binding | Static binding | Dynamic binding |
| Performance | Faster | Slight overhead (vtable lookup) |
6. What is a class vs an object?
- Class — Blueprint/template defining attributes and methods. Exists at compile time.
- Object — Instance of a class. Exists at runtime in memory.
class Car: # class — blueprint
def __init__(self, make, model):
self.make = make
self.model = model
def describe(self):
return f"{self.make} {self.model}"
my_car = Car("Toyota", "Camry") # object — instance
your_car = Car("Honda", "Civic") # another object from same class
7. What is an abstract class vs an interface?
| Abstract Class | Interface | |
|---|---|---|
| Instantiation | Cannot instantiate | Cannot instantiate |
| Methods | Can have concrete + abstract | Abstract only (Java 7); default/static methods (Java 8+) |
| Fields | Can have instance fields | Constants only (Java) |
| Constructor | Yes | No |
| Inheritance | Single (Java/C#) | Multiple |
| Use when | Partial shared implementation | Define a contract |
// Abstract class — partial implementation
abstract class Vehicle {
protected int speed;
abstract void accelerate(); // must override
void stop() { speed = 0; } // concrete — shared behavior
}
// Interface — pure contract
interface Flyable {
void fly();
default String mode() { return "air"; } // Java 8+ default
}
class FlyingCar extends Vehicle implements Flyable {
@Override public void accelerate() { speed += 10; }
@Override public void fly() { System.out.println("Flying!"); }
}
8. What is method overloading vs method overriding?
Overloading (compile-time): Same method name, different parameters in the same class.
void print(int x) { }
void print(String s) { }
void print(int x, int y) { } // same name, different signatures
Overriding (runtime): Same method signature in child class replaces parent's behavior.
class Animal { void speak() { System.out.println("..."); } }
class Cat extends Animal {
@Override void speak() { System.out.println("Meow"); }
}
| Overloading | Overriding | |
|---|---|---|
| Class | Same class | Parent + child class |
| Signature | Must differ | Must be identical |
| Return type | Can differ | Must match (or covariant) |
| Binding | Static | Dynamic |
| Polymorphism | Compile-time | Runtime |
9. What is the super keyword?
super refers to the parent class. Used to:
- Call parent constructor:
super(args) - Call parent method:
super.methodName() - Access parent field:
super.fieldName
class Animal {
String name;
Animal(String name) { this.name = name; }
void speak() { System.out.println("Sound from " + name); }
}
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name); // calls Animal constructor
this.breed = breed;
}
@Override
void speak() {
super.speak(); // calls Animal.speak()
System.out.println(name + " barks");
}
}
10. What is the diamond problem?
The diamond problem occurs with multiple inheritance when two parent classes define the same method, and a child class inherits from both.
A
/ \
B C B.speak() and C.speak() both override A.speak()
\ /
D Which speak() does D inherit?
Java/C# solve this by disallowing multiple class inheritance. Python uses MRO (Method Resolution Order) — C3 linearization algorithm.
class A:
def speak(self): return "A"
class B(A):
def speak(self): return "B"
class C(A):
def speak(self): return "C"
class D(B, C): pass
print(D().speak()) # "B" — follows MRO: D → B → C → A
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, ...)
SOLID Principles
11. What are the SOLID principles?
SOLID is an acronym for five object-oriented design principles that make software more maintainable and extensible.
| Letter | Principle | One-liner |
|---|---|---|
| S | Single Responsibility | A class has one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for base types |
| I | Interface Segregation | Clients shouldn't depend on unused methods |
| D | Dependency Inversion | Depend on abstractions, not concretions |
12. Explain Single Responsibility Principle (SRP)
A class should have one reason to change — it should do one thing well.
# WRONG — UserService handles 3 responsibilities
class UserService:
def get_user(self, id): pass
def save_to_database(self, user): pass # DB responsibility
def send_welcome_email(self, user): pass # Email responsibility
# RIGHT — separate responsibilities
class UserRepository:
def get_user(self, id): pass
def save(self, user): pass
class EmailService:
def send_welcome_email(self, user): pass
class UserService:
def __init__(self, repo: UserRepository, email: EmailService):
self.repo = repo
self.email = email
13. Explain Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification — add new behavior without changing existing code.
# WRONG — adding a new shape requires modifying AreaCalculator
class AreaCalculator:
def calculate(self, shape):
if shape.type == "circle": return 3.14 * shape.radius ** 2
if shape.type == "rectangle": return shape.width * shape.height
# every new shape = modify this class
# RIGHT — extend via polymorphism
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: pass
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
class Triangle(Shape): # new shape — no existing code changes
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h
def total_area(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes)
14. Explain Liskov Substitution Principle (LSP)
Objects of a subclass must be usable wherever the parent class is expected, without breaking the program.
# WRONG — Square violates LSP: setting width also sets height, breaks Rectangle contract
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w): self.width = self.height = w # breaks LSP
def set_height(self, h): self.width = self.height = h
def area_should_be_width_times_height(r: Rectangle):
r.set_width(5)
r.set_height(4)
assert r.area() == 20 # FAILS for Square — area is 16 (4*4)
# RIGHT — Square should NOT inherit Rectangle; both inherit Shape
LSP violation signs:
- Subclass throws
NotImplementedExceptionfor parent methods - Subclass weakens preconditions or strengthens postconditions
- Code checks
isinstanceto handle subtype differently
15. Explain Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use. Split fat interfaces into smaller, specific ones.
# WRONG — Robot must implement eat() which it cannot do
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass # robots don't eat!
# RIGHT — segregate into focused interfaces
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class HumanWorker(Workable, Eatable):
def work(self): print("Working")
def eat(self): print("Eating lunch")
class Robot(Workable):
def work(self): print("Processing")
# doesn't need to implement eat()
16. Explain Dependency Inversion Principle (DIP)
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
# WRONG — OrderService depends on concrete MySQLDatabase
class MySQLDatabase:
def save(self, order): pass
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # hardcoded dependency
# RIGHT — depend on abstraction
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, order): pass
class MySQLDatabase(Database):
def save(self, order): print("Saving to MySQL")
class MongoDatabase(Database):
def save(self, order): print("Saving to MongoDB")
class OrderService:
def __init__(self, db: Database): # inject dependency
self.db = db
# Swap databases without changing OrderService
service = OrderService(MySQLDatabase())
service = OrderService(MongoDatabase())
Classes and Objects Deep Dive
17. What is a constructor? Types?
A constructor is a special method called when an object is created, used to initialize object state.
class Person {
String name;
int age;
// Default constructor
Person() { this.name = "Unknown"; this.age = 0; }
// Parameterized constructor
Person(String name, int age) {
this.name = name;
this.age = age;
}
// Copy constructor
Person(Person other) {
this.name = other.name;
this.age = other.age;
}
}
| Type | When used |
|---|---|
| Default | No-arg; sets default values |
| Parameterized | Initialize with provided values |
| Copy | Create a copy of another object (C++) |
| Static factory | Named alternatives to constructors |
18. What is the this keyword?
this refers to the current instance of the class.
class Employee {
String name;
double salary;
Employee(String name, double salary) {
this.name = name; // disambiguates from parameter
this.salary = salary;
}
Employee promote(double raise) {
this.salary += raise;
return this; // method chaining
}
void printInfo() {
System.out.println(this.name + ": $" + this.salary);
}
}
Uses of this:
- Differentiate instance variable from constructor/method parameter
- Call another constructor from the same class (
this(args)) - Return current object for method chaining
- Pass current object as argument to another method
19. What is static vs instance members?
| Instance | Static | |
|---|---|---|
| Belongs to | Object (instance) | Class itself |
| Memory | Per object | Once per class |
| Access | Via object reference | Via class name |
Use this |
Yes | No |
| When to use | Object state/behavior | Shared state, utilities |
class Counter:
count = 0 # static — shared across all instances
def __init__(self):
Counter.count += 1
self.id = Counter.count # instance — unique per object
c1 = Counter() # Counter.count = 1, c1.id = 1
c2 = Counter() # Counter.count = 2, c2.id = 2
print(Counter.count) # 2 — class-level
20. What is an inner class? Types?
An inner class is a class defined inside another class.
class Outer {
private int x = 10;
// Non-static inner class — has access to outer instance
class Inner {
void display() { System.out.println(x); }
}
// Static nested class — no access to outer instance fields
static class StaticNested {
void greet() { System.out.println("Static nested"); }
}
void method() {
// Local inner class — defined inside a method
class Local {
void run() { System.out.println("Local class"); }
}
new Local().run();
}
Runnable getLambda() {
// Anonymous class — inline implementation
return new Runnable() {
@Override public void run() { System.out.println("Anonymous"); }
};
}
}
21. What is composition vs inheritance? When to use each?
Favor composition over inheritance — Gang of Four
Inheritance ("is-a"): Dog is an Animal
Composition ("has-a"): Car has an Engine
# Inheritance — works when true "is-a" relationship
class Animal:
def breathe(self): return "breathing"
class Dog(Animal):
def bark(self): return "woof"
# Composition — preferred for flexibility
class Engine:
def start(self): return "vroom"
class Car:
def __init__(self):
self.engine = Engine() # has-a
def drive(self):
return self.engine.start()
Use inheritance when:
- True "is-a" relationship exists
- Child truly represents a specialization of parent
- Liskov Substitution holds
Use composition when:
- "Has-a" or "uses-a" relationship
- Need flexibility to swap implementations
- Inheritance would create fragile coupling
22. What is method hiding vs method overriding?
class Parent {
static void staticMethod() { System.out.println("Parent static"); }
void instanceMethod() { System.out.println("Parent instance"); }
}
class Child extends Parent {
// Method HIDING — static methods are NOT overridden, they're hidden
static void staticMethod() { System.out.println("Child static"); }
// Method OVERRIDING — dynamic dispatch applies
@Override
void instanceMethod() { System.out.println("Child instance"); }
}
Parent ref = new Child();
ref.staticMethod(); // "Parent static" — resolved at compile time (hiding)
ref.instanceMethod(); // "Child instance" — resolved at runtime (overriding)
23. What are access modifiers?
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public |
✓ | ✓ | ✓ | ✓ |
protected |
✓ | ✓ | ✓ | ✗ |
| (default) | ✓ | ✓ | ✗ | ✗ |
private |
✓ | ✗ | ✗ | ✗ |
Best practice: use the most restrictive access possible (principle of least privilege).
24. What is a sealed/final class?
A final (Java/C#: sealed) class cannot be subclassed. Used when inheritance would break invariants.
public final class String { } // Java String is final
// class MyString extends String { } // compile error
// C#
public sealed class Singleton { }
Why seal a class?
- Security — prevent malicious subclasses
- Performance — JIT can inline calls (no vtable)
- Immutability guarantees (e.g.,
java.lang.String)
25. What is object cloning? Shallow vs deep copy?
class Address {
String city;
Address(String city) { this.city = city; }
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
// Shallow copy — copies references
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // address reference is shared!
}
// Deep copy — copies nested objects
Person deepCopy() {
return new Person(this.name, new Address(this.address.city));
}
}
Person original = new Person("Alice", new Address("NYC"));
Person shallow = (Person) original.clone();
shallow.address.city = "LA"; // ALSO changes original.address.city!
Person deep = original.deepCopy();
deep.address.city = "Chicago"; // does NOT affect original
Design Patterns
26. What is the Singleton pattern? How to implement thread-safely?
Ensures a class has only one instance and provides a global access point.
// Thread-safe Singleton using initialization-on-demand holder
public class DatabaseConnection {
private DatabaseConnection() { }
private static class Holder {
static final DatabaseConnection INSTANCE = new DatabaseConnection();
}
public static DatabaseConnection getInstance() {
return Holder.INSTANCE; // JVM guarantees thread-safe initialization
}
public void query(String sql) { /* ... */ }
}
// Usage
DatabaseConnection.getInstance().query("SELECT * FROM users");
Anti-pattern warning: Singletons introduce global state and make testing harder. Prefer dependency injection.
27. What is the Factory pattern?
Factory method pattern delegates object creation to subclasses.
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str): pass
class EmailNotification(Notification):
def send(self, message: str): print(f"Email: {message}")
class SMSNotification(Notification):
def send(self, message: str): print(f"SMS: {message}")
class PushNotification(Notification):
def send(self, message: str): print(f"Push: {message}")
class NotificationFactory:
@staticmethod
def create(type_: str) -> Notification:
options = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
cls = options.get(type_)
if not cls:
raise ValueError(f"Unknown notification type: {type_}")
return cls()
notif = NotificationFactory.create("email")
notif.send("Hello!")
28. What is the Observer pattern?
Defines a one-to-many dependency so when one object changes state, all dependents are notified automatically.
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event: str): pass
class EventSource:
def __init__(self):
self._observers: list[Observer] = []
def subscribe(self, observer: Observer):
self._observers.append(observer)
def unsubscribe(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str):
for observer in self._observers:
observer.update(event)
class Logger(Observer):
def update(self, event: str): print(f"[LOG] {event}")
class AlertService(Observer):
def update(self, event: str): print(f"[ALERT] {event}")
source = EventSource()
source.subscribe(Logger())
source.subscribe(AlertService())
source.notify("User logged in") # both receive event
Real-world use: Event systems, UI frameworks (MVC), pub/sub message queues.
29. What is the Strategy pattern?
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: pass
class BubbleSort(SortStrategy):
def sort(self, data): return sorted(data) # simplified
class QuickSort(SortStrategy):
def sort(self, data): return sorted(data, key=lambda x: x)
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data)
sorter = Sorter(BubbleSort())
sorter.sort([3, 1, 2])
sorter.set_strategy(QuickSort()) # swap algorithm without changing Sorter
sorter.sort([3, 1, 2])
30. What is the Decorator pattern?
Attaches additional responsibilities to an object dynamically without modifying its class.
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float: pass
@abstractmethod
def description(self) -> str: pass
class Espresso(Coffee):
def cost(self) -> float: return 1.50
def description(self) -> str: return "Espresso"
class MilkDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float: return self._coffee.cost() + 0.25
def description(self) -> str: return self._coffee.description() + ", Milk"
class VanillaDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float: return self._coffee.cost() + 0.50
def description(self) -> str: return self._coffee.description() + ", Vanilla"
drink = VanillaDecorator(MilkDecorator(Espresso()))
print(drink.description()) # Espresso, Milk, Vanilla
print(drink.cost()) # 2.25
Advanced OOP Concepts
31. What is a mixin?
A mixin is a class providing methods to other classes through multiple inheritance without being a standalone class.
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class LogMixin:
def log(self, message: str):
print(f"[{self.__class__.__name__}] {message}")
class User(JSONMixin, LogMixin):
def __init__(self, name: str, email: str):
self.name = name
self.email = email
user = User("Alice", "alice@example.com")
print(user.to_json()) # {"name": "Alice", "email": "alice@example.com"}
user.log("User created") # [User] User created
Mixins are widely used in Python (Django class-based views), Ruby, and other languages.
32. What is duck typing?
"If it walks like a duck and quacks like a duck, it's a duck." — Python checks for behavior, not type.
class Dog:
def speak(self): return "Woof"
class Cat:
def speak(self): return "Meow"
class Robot:
def speak(self): return "Beep boop"
def make_speak(entity):
print(entity.speak()) # no isinstance check needed
for speaker in [Dog(), Cat(), Robot()]:
make_speak(speaker) # works as long as speak() exists
33. What is method resolution order (MRO)?
MRO determines the order Python searches for a method in the inheritance hierarchy using the C3 linearization algorithm.
class A:
def hello(self): return "A"
class B(A):
def hello(self): return "B"
class C(A):
def hello(self): return "C"
class D(B, C):
pass
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
print(D().hello()) # "B" — follows MRO order
34. What are magic/dunder methods?
Special methods surrounded by double underscores (__) that Python calls implicitly.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __len__(self):
return 2
def __iter__(self):
yield self.x
yield self.y
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6) — calls __add__
print(v1 == v1) # True — calls __eq__
print(len(v1)) # 2 — calls __len__
x, y = v1 # unpacking — calls __iter__
Common dunder methods:
| Method | When called |
|---|---|
__init__ |
Object creation |
__str__ |
str(obj) |
__repr__ |
repr(obj), debugging |
__len__ |
len(obj) |
__eq__ |
obj1 == obj2 |
__lt__ |
obj1 < obj2 |
__hash__ |
hash(obj), dict keys |
__enter__/__exit__ |
with statement |
__iter__/__next__ |
iteration |
__getitem__ |
obj[key] |
__call__ |
obj() |
35. What is an abstract method vs virtual method?
// C#
abstract class Animal {
// Abstract — no implementation, MUST override in subclass
public abstract void Speak();
// Virtual — has default implementation, CAN override
public virtual void Breathe() => Console.WriteLine("Breathing");
}
class Dog : Animal {
public override void Speak() => Console.WriteLine("Woof");
// Breathe() — can inherit parent's or override
}
| Abstract | Virtual | |
|---|---|---|
| Implementation | None | Has default |
| Override required | Yes | No |
| In abstract class | Must be abstract | Can be in any class |
36. What is covariance and contravariance?
- Covariance — return type can be more derived type (child)
- Contravariance — parameter type can be more base type (parent)
class Animal { }
class Dog extends Animal { }
class AnimalFactory {
Animal create() { return new Animal(); }
}
class DogFactory extends AnimalFactory {
@Override
Dog create() { return new Dog(); } // covariant return type — allowed in Java
}
In generics, Java uses wildcards: ? extends T (covariant), ? super T (contravariant).
37. What is a fluent interface / method chaining?
A pattern where methods return this to allow chaining multiple calls.
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._limit = None
def from_table(self, table: str) -> "QueryBuilder":
self._table = table
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def build(self) -> str:
sql = f"SELECT * FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
query = (QueryBuilder()
.from_table("users")
.where("age > 18")
.where("active = 1")
.limit(10)
.build())
# SELECT * FROM users WHERE age > 18 AND active = 1 LIMIT 10
38. What is dependency injection (DI)?
DI is a design pattern where dependencies are provided to an object rather than created within it.
# Without DI — hard to test, hard to swap implementations
class OrderService:
def __init__(self):
self.db = ProductionDatabase() # hardcoded!
# With DI — testable, flexible
class OrderService:
def __init__(self, db: Database): # injected
self.db = db
# Production
service = OrderService(MySQLDatabase())
# Test
service = OrderService(InMemoryDatabase()) # no production DB needed
Types of injection:
- Constructor injection — via constructor (preferred)
- Setter injection — via setter method
- Interface injection — via interface method
39. What is the Law of Demeter (LoD)?
"Don't talk to strangers." A method should only call methods on:
- Itself (
this) - Its direct fields
- Parameters passed to it
- Objects it creates
# WRONG — violates LoD (train wreck)
def get_city(customer):
return customer.get_address().get_location().get_city()
# RIGHT — tell, don't ask
def get_city(customer):
return customer.get_city() # Customer knows its city
class Customer:
def get_city(self):
return self._address.location.city # internalized
40. What is object cohesion and coupling?
| Good | Bad | |
|---|---|---|
| Cohesion | High — class does one focused thing | Low — class does many unrelated things |
| Coupling | Loose — classes have minimal dependencies | Tight — changes in one class break others |
Goal: High cohesion, loose coupling.
# Tight coupling — PaymentService knows MySQL internals
class PaymentService:
def pay(self, amount):
import mysql.connector
conn = mysql.connector.connect(host="localhost", ...)
# Loose coupling — PaymentService depends on abstraction
class PaymentService:
def __init__(self, db: Database):
self.db = db
def pay(self, amount):
self.db.save({"amount": amount})
Practical & Language-Specific Questions
41. How does Python handle multiple inheritance?
Python uses MRO (C3 linearization) — left-to-right, depth-first, with consistency constraints.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
# MRO: D → B → C → A → object
super() respects MRO — always calls the next class in MRO, not necessarily the direct parent.
42. What is @property in Python?
Allows method access via attribute syntax while maintaining encapsulation.
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
t = Temperature(25)
print(t.fahrenheit) # 77.0 — accessed like attribute, not t.fahrenheit()
t.celsius = -300 # raises ValueError
43. What is the difference between __str__ and __repr__?
class User:
def __init__(self, name, id):
self.name, self.id = name, id
def __str__(self):
return f"User: {self.name}" # human-readable
def __repr__(self):
return f"User(name={self.name!r}, id={self.id!r})" # unambiguous, for debugging
u = User("Alice", 42)
print(str(u)) # "User: Alice"
print(repr(u)) # "User(name='Alice', id=42)"
print([u]) # Uses repr in containers: [User(name='Alice', id=42)]
44. How is OOP different in Python vs Java?
| Feature | Python | Java |
|---|---|---|
| Access modifiers | Convention (_, __) |
Enforced by compiler |
| Multiple inheritance | Yes | No (interfaces only) |
| Duck typing | Yes | No (static typed) |
| Abstract classes | abc.ABC |
abstract keyword |
| Properties | @property |
Getters/setters |
| Metaclasses | Yes | Limited |
| All classes inherit | object |
Object |
45. What is object serialization?
Converting an object to a format that can be stored or transmitted (JSON, XML, binary), and reconstructing it.
import json
from dataclasses import dataclass, asdict
@dataclass
class User:
name: str
age: int
email: str
user = User("Alice", 30, "alice@example.com")
# Serialize
data = json.dumps(asdict(user))
print(data) # '{"name": "Alice", "age": 30, "email": "alice@example.com"}'
# Deserialize
user2 = User(**json.loads(data))
Java uses implements Serializable. C# uses [Serializable] attribute.
46. What is the difference between == and .equals() in Java?
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — different object references
System.out.println(a.equals(b)); // true — same content
// But for string literals (string pool):
String c = "hello";
String d = "hello";
System.out.println(c == d); // true — same pool object
Rule: Always use .equals() for content comparison. == checks reference equality.
47. What is a generic class?
Generics allow writing type-safe code that works with different types without casting.
// Without generics — unsafe, requires casting
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // ClassCastException at runtime if wrong
// With generics — type-safe at compile time
List<String> strings = new ArrayList<>();
strings.add("hello");
String str = strings.get(0); // no cast needed
// Generic class
class Pair<A, B> {
private final A first;
private final B second;
Pair(A first, B second) { this.first = first; this.second = second; }
A getFirst() { return first; }
B getSecond() { return second; }
}
Pair<String, Integer> pair = new Pair<>("age", 30);
48. What are some OOP best practices?
| Practice | Description |
|---|---|
| Favor composition over inheritance | More flexible, less coupling |
| Program to interfaces | Depend on abstractions |
| Keep classes small | SRP — one reason to change |
| Avoid deep inheritance | 2-3 levels max |
| Immutability | Make fields final/readonly where possible |
| Encapsulate what varies | Isolate change-prone code |
| Tell, don't ask | Tell objects to do things, don't query and decide |
| Least knowledge (LoD) | Only talk to immediate friends |
49. What OOP patterns are commonly used in real-world applications?
| Pattern | Real-world use |
|---|---|
| Singleton | DB connection pools, config, loggers |
| Factory/Abstract Factory | UI widget creation, payment processors |
| Builder | SQL query builders, HTTP request builders |
| Observer | Event systems, UI frameworks, pub/sub |
| Strategy | Sorting, payment methods, compression algorithms |
| Decorator | Middleware stacks, logging wrappers, I/O streams |
| Repository | Data access layer, ORM abstractions |
| Command | Undo/redo, job queues, transactional scripts |
| Proxy | Lazy loading, caching, access control |
| Facade | Simplifying complex subsystems (e.g., API clients) |
50. How do you design an OOP solution? (Common interview scenario)
Design a Parking Lot system:
from enum import Enum
from abc import ABC, abstractmethod
from datetime import datetime
class VehicleType(Enum):
MOTORCYCLE, CAR, TRUCK = "motorcycle", "car", "truck"
class Vehicle:
def __init__(self, plate: str, type_: VehicleType):
self.plate = plate
self.type = type_
class ParkingSpot:
def __init__(self, id: str, type_: VehicleType):
self.id = id
self.type = type_
self.vehicle: Vehicle | None = None
def is_available(self) -> bool:
return self.vehicle is None
def park(self, vehicle: Vehicle):
if not self.is_available():
raise RuntimeError("Spot occupied")
self.vehicle = vehicle
def vacate(self) -> Vehicle:
vehicle = self.vehicle
self.vehicle = None
return vehicle
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, hours: float, vehicle_type: VehicleType) -> float: pass
class HourlyPricing(PricingStrategy):
RATES = {VehicleType.MOTORCYCLE: 1.0, VehicleType.CAR: 2.0, VehicleType.TRUCK: 3.5}
def calculate(self, hours: float, vehicle_type: VehicleType) -> float:
return hours * self.RATES[vehicle_type]
class ParkingLot:
def __init__(self, spots: list[ParkingSpot], pricing: PricingStrategy):
self.spots = spots
self.pricing = pricing
self._tickets: dict[str, tuple[ParkingSpot, datetime]] = {}
def park(self, vehicle: Vehicle) -> str:
spot = next((s for s in self.spots
if s.type == vehicle.type and s.is_available()), None)
if not spot:
raise RuntimeError("No available spots")
spot.park(vehicle)
self._tickets[vehicle.plate] = (spot, datetime.now())
return spot.id
def exit(self, plate: str) -> float:
spot, entry_time = self._tickets.pop(plate)
vehicle = spot.vacate()
hours = (datetime.now() - entry_time).seconds / 3600
return self.pricing.calculate(max(hours, 1/60), vehicle.type)
Approach for any OOP design question:
- Identify entities (nouns) → classes
- Identify behaviors (verbs) → methods
- Define relationships (is-a vs has-a)
- Apply SOLID principles
- Use design patterns where appropriate
Anti-Patterns to Avoid
| Anti-pattern | Problem | Fix |
|---|---|---|
| God Object | One class knows/does everything | SRP — split responsibilities |
| Anemic Domain Model | Classes with only data, no behavior | Add domain logic to entities |
| Deep Inheritance | 5+ levels of inheritance | Prefer composition, max 2-3 levels |
| Inappropriate Intimacy | Classes know too much about each other | Encapsulate, use interfaces |
| Refused Bequest | Subclass doesn't use inherited members | Violates LSP — restructure |
| Premature Generalization | Over-engineered for hypothetical future | YAGNI — do what's needed |
| Spaghetti Inheritance | Multiple inheritance misused | Use interfaces + composition |
| Singleton Abuse | Global state everywhere | Use dependency injection |
OOP vs Other Paradigms
| Paradigm | Key Concept | Best for |
|---|---|---|
| OOP | Objects, encapsulation, polymorphism | Business apps, UI, games |
| Functional | Pure functions, immutability, composition | Data transforms, concurrent systems |
| Procedural | Sequences of instructions | Scripts, simple programs |
| Reactive | Data streams, async propagation | UI events, real-time data |
Modern languages blend paradigms — Python, Kotlin, and Scala support both OOP and functional styles.
FAQ
Q: Is everything in Python an object?
Yes. Every value in Python — integers, strings, functions, classes — is an object. type(42) → <class 'int'>. Even None is an object (<class 'NoneType'>).
Q: Can you override a private method?
No. Private methods are not visible to subclasses, so there's nothing to override. In Python, name mangling (__method → _ClassName__method) prevents accidental access but doesn't prevent override if you know the mangled name.
Q: What's the difference between aggregation and composition?
Both are "has-a" relationships:
- Aggregation — weak ownership. Parts can exist without the whole.
DepartmenthasEmployees— employees survive if department is disbanded. - Composition — strong ownership. Parts cannot exist without the whole.
HousehasRooms— rooms don't exist without the house.
Q: When should you NOT use OOP?
- Simple scripts with no shared state
- Pure data pipelines and transformations (functional style fits better)
- Performance-critical systems where vtable dispatch overhead matters
- Configuration/glue code
Q: How do you explain OOP to a non-technical person?
OOP is like building with LEGO bricks. Each brick (object) has a specific shape and purpose. You can reuse the same brick in different builds (code reuse). Some bricks snap together only in certain ways (encapsulation). You can build a bigger, specialized brick on top of a basic one (inheritance). The same colored brick can play different roles in different builds (polymorphism).
Q: What's the best way to practice OOP design?
- Design common systems: Parking lot, Library, ATM, Chess game, Hotel booking
- Refactor procedural code into OOP
- Implement design patterns from scratch
- Read open-source OOP codebases (Spring, Django)
- Do LeetCode Object-Oriented Design problems