Top 20 OOP Interview Questions and Answers [2025]

Master OOP interview questions with detailed answers and Python examples. Cover encapsulation, inheritance, polymorphism, abstraction, SOLID principles, and design patterns for coding interviews

📅 Published: March 28, 2025 ✏️ Updated: April 15, 2025 By Ojaswi Athghara
#oop #interview #python #questions #coding

Top 20 OOP Interview Questions and Answers 2025

The Interview That Changed Everything

"Tell me about the four pillars of OOP," the interviewer asked.

I froze. I knew encapsulation, inheritance... but what were the other two? I fumbled through an answer about "classes and objects" (wrong!).

I didn't get that job.

After that interview, I spent weeks mastering OOP concepts. I practiced every common OOP interview question until I could answer them in my sleep.

Six months later, I aced three back-to-back interviews. Different companies, same questions. I was prepared.

Today, I'm sharing the Top 20 OOP interview questions I've encountered—with detailed answers and code examples you can use in your next interview.

Part 1: Core OOP Concepts

Q1: What are the four pillars of OOP?

Answer:

The four pillars of Object-Oriented Programming are:

  1. Encapsulation - Bundling data and methods together; hiding internal details
  2. Inheritance - Reusing code through parent-child class relationships
  3. Polymorphism - Same interface, different implementations
  4. Abstraction - Hiding complex implementation details; showing only essentials

Example:

# 1. Encapsulation
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        self.__balance += amount  # Controlled access

# 2. Inheritance
class SavingsAccount(BankAccount):
    def __init__(self, balance, interest_rate):
        super().__init__(balance)
        self.interest_rate = interest_rate

# 3. Polymorphism
class Dog:
    def speak(self): return "Woof!"

class Cat:
    def speak(self): return "Meow!"

def make_sound(animal):  # Same method, different behavior
    print(animal.speak())

# 4. Abstraction
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):  # Abstract method
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):  # Concrete implementation
        return 3.14 * self.radius ** 2

Follow-up: "Give a real-world example of each pillar."


Q2: What is the difference between a class and an object?

Answer:

  • Class is a blueprint or template that defines attributes and methods
  • Object is an instance of a class; the actual entity created from the blueprint

Analogy: Class is like a cookie cutter, objects are the actual cookies.

# Class (blueprint)
class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def start(self):
        print(f"{self.brand} {self.model} is starting...")

# Objects (instances)
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Accord")

# Each object has its own data
car1.start()  # Toyota Camry is starting...
car2.start()  # Honda Accord is starting...

Q3: What is encapsulation? Why is it important?

Answer:

Encapsulation is bundling data (attributes) and methods that operate on that data together in a class, and restricting direct access to some components.

Benefits:

  • Data protection - Prevents direct modification
  • Validation - Control how data is accessed/modified
  • Flexibility - Change internal implementation without affecting external code
  • Security - Hide sensitive data
class User:
    def __init__(self, username, password):
        self.username = username
        self.__password = password  # Private (name mangling)
    
    def verify_password(self, input_password):
        """Controlled access to password"""
        return self.__password == input_password
    
    def change_password(self, old_pass, new_pass):
        """Validated password change"""
        if not self.verify_password(old_pass):
            raise ValueError("Current password is incorrect")
        
        if len(new_pass) < 8:
            raise ValueError("Password must be at least 8 characters")
        
        self.__password = new_pass
        print("Password changed successfully")

user = User("alice", "secret123")

# Cannot access password directly
# print(user.__password)  # AttributeError!

# Must use methods (controlled access)
print(user.verify_password("secret123"))  # True
user.change_password("secret123", "newSecret456")

Q4: Explain inheritance with an example. What are its types?

Answer:

Inheritance allows a class (child/derived) to inherit attributes and methods from another class (parent/base), promoting code reuse.

Types of inheritance:

  1. Single - One parent, one child
  2. Multiple - Multiple parents, one child
  3. Multilevel - Grandparent → Parent → Child
  4. Hierarchical - One parent, multiple children
  5. Hybrid - Combination of above
# Single Inheritance
class Animal:
    def eat(self):
        print("Eating...")

class Dog(Animal):
    def bark(self):
        print("Woof!")

dog = Dog()
dog.eat()   # Inherited
dog.bark()  # Own method

# Multiple Inheritance
class Flyable:
    def fly(self):
        print("Flying...")

class Swimmable:
    def swim(self):
        print("Swimming...")

class Duck(Animal, Flyable, Swimmable):
    pass

duck = Duck()
duck.eat()   # From Animal
duck.fly()   # From Flyable
duck.swim()  # From Swimmable

# Multilevel Inheritance
class Vehicle:
    def start(self):
        print("Starting vehicle...")

class Car(Vehicle):
    def drive(self):
        print("Driving car...")

class ElectricCar(Car):
    def charge(self):
        print("Charging battery...")

tesla = ElectricCar()
tesla.start()   # From Vehicle
tesla.drive()   # From Car
tesla.charge()  # Own method

Follow-up: "What is the diamond problem in multiple inheritance?"

Answer: When a class inherits from two classes that share a common parent, causing ambiguity. Python resolves this using Method Resolution Order (MRO).


Q5: What is polymorphism? Explain with examples.

Answer:

Polymorphism means "many forms" - same interface, different implementations.

Two types:

  1. Compile-time (Method Overloading) - Same method name, different parameters
  2. Runtime (Method Overriding) - Child class redefines parent method
# Runtime Polymorphism (Method Overriding)
class Shape:
    def area(self):
        return 0

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):  # Override
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):  # Override
        return 3.14 * self.radius ** 2

# Polymorphic behavior
def print_area(shape: Shape):
    print(f"Area: {shape.area()}")

shapes = [Rectangle(5, 10), Circle(7)]

for shape in shapes:
    print_area(shape)  # Same method call, different outputs

# Output:
# Area: 50
# Area: 153.86

Part 2: Advanced OOP Concepts

Q6: What is abstraction? How is it different from encapsulation?

Answer:

Abstraction hides complex implementation details and shows only essential features.

Difference:

  • Abstraction - What to do (interface)
  • Encapsulation - How to do it (implementation hiding)
from abc import ABC, abstractmethod

# Abstraction: Define WHAT to do
class PaymentMethod(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

# Concrete implementations define HOW
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.__card_number = card_number  # Encapsulation
    
    def process_payment(self, amount):
        print(f"Processing ${amount} via credit card")
        # Complex payment logic hidden

class PayPal(PaymentMethod):
    def __init__(self, email):
        self.__email = email  # Encapsulation
    
    def process_payment(self, amount):
        print(f"Processing ${amount} via PayPal")
        # Complex PayPal logic hidden

# User doesn't know HOW payment is processed
def checkout(payment: PaymentMethod, amount):
    payment.process_payment(amount)

checkout(CreditCard("****1234"), 99.99)
checkout(PayPal("user@email.com"), 149.99)

Q7: What are abstract classes? When would you use them?

Answer:

Abstract class is a class that:

  • Cannot be instantiated directly
  • Contains abstract methods (no implementation) that must be implemented by child classes
  • Can have concrete methods (with implementation)

Use when:

  • You want to define a common interface for related classes
  • You have shared implementation that subclasses can use
  • You want to enforce certain methods in all subclasses
from abc import ABC, abstractmethod

class Database(ABC):
    def __init__(self, connection_string):
        self.connection_string = connection_string
    
    # Concrete method (shared by all)
    def log_query(self, query):
        print(f"Executing: {query}")
    
    # Abstract methods (must implement)
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def execute(self, query):
        pass

class MySQLDatabase(Database):
    def connect(self):
        print(f"Connecting to MySQL: {self.connection_string}")
    
    def execute(self, query):
        self.log_query(query)  # Use inherited method
        print("Executing MySQL query")

class PostgreSQLDatabase(Database):
    def connect(self):
        print(f"Connecting to PostgreSQL: {self.connection_string}")
    
    def execute(self, query):
        self.log_query(query)  # Use inherited method
        print("Executing PostgreSQL query")

# Cannot instantiate abstract class
# db = Database("connection")  # TypeError!

# Must use concrete classes
mysql = MySQLDatabase("mysql://localhost:3306")
postgres = PostgreSQLDatabase("postgresql://localhost:5432")

mysql.connect()
mysql.execute("SELECT * FROM users")

Q8: What are interfaces? How are they different from abstract classes?

Answer:

FeatureAbstract ClassInterface
ImplementationCan have concrete methodsOnly method signatures (Python uses ABC)
ConstructorYesNo
Multiple InheritanceNo (single)Yes (multiple)
When to UseShared implementation + interfacePure contract

Python Note: Python doesn't have formal interfaces, but uses abstract base classes with all abstract methods.

from abc import ABC, abstractmethod

# "Interface" in Python (abstract class with all abstract methods)
class Drawable(ABC):
    @abstractmethod
    def draw(self):
        pass

class Clickable(ABC):
    @abstractmethod
    def on_click(self):
        pass

# Class can "implement" multiple "interfaces"
class Button(Drawable, Clickable):
    def draw(self):
        print("Drawing button...")
    
    def on_click(self):
        print("Button clicked!")

button = Button()
button.draw()
button.on_click()

Q9: What is method overriding vs method overloading?

Answer:

Method Overriding:

  • Child class redefines parent class method
  • Same method name, same parameters
  • Runtime polymorphism

Method Overloading:

  • Multiple methods with same name, different parameters
  • Compile-time polymorphism
  • Note: Python doesn't support traditional overloading (last definition wins)
# Method Overriding
class Animal:
    def make_sound(self):
        return "Some sound"

class Dog(Animal):
    def make_sound(self):  # Override
        return "Woof!"

dog = Dog()
print(dog.make_sound())  # "Woof!"


# Method Overloading (Python workaround using default arguments)
class Calculator:
    def add(self, a, b=None, c=None):
        if b is None:
            return a
        elif c is None:
            return a + b
        else:
            return a + b + c

calc = Calculator()
print(calc.add(5))         # 5
print(calc.add(5, 3))      # 8
print(calc.add(5, 3, 2))   # 10

Q10: What is super() and why is it used?

Answer:

super() is used to call methods from a parent class. It's commonly used to:

  • Call parent constructor
  • Extend parent method behavior
  • Navigate MRO (Method Resolution Order) in multiple inheritance
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        print(f"Vehicle initialized: {brand} {model}")
    
    def start(self):
        print("Starting vehicle...")

class Car(Vehicle):
    def __init__(self, brand, model, doors):
        super().__init__(brand, model)  # Call parent constructor
        self.doors = doors
        print(f"Car initialized with {doors} doors")
    
    def start(self):
        super().start()  # Call parent method
        print("Car-specific start logic")

car = Car("Toyota", "Camry", 4)
# Output:
# Vehicle initialized: Toyota Camry
# Car initialized with 4 doors

car.start()
# Output:
# Starting vehicle...
# Car-specific start logic

Part 3: Design Principles & Patterns

Q11: Explain SOLID principles briefly.

Answer:

  1. S - Single Responsibility Principle: Class should have one reason to change
  2. O - Open/Closed Principle: Open for extension, closed for modification
  3. L - Liskov Substitution Principle: Subclasses should be substitutable for their base class
  4. I - Interface Segregation Principle: Many specific interfaces > one general interface
  5. D - Dependency Inversion Principle: Depend on abstractions, not concretions
# S - Single Responsibility
class User:
    pass  # Only user data

class UserRepository:
    pass  # Only database operations

class EmailService:
    pass  # Only email sending

# O - Open/Closed
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount): pass

class CreditCard(PaymentMethod):  # Extend, don't modify
    def pay(self, amount):
        print(f"Paying ${amount} via credit card")

# L - Liskov Substitution
def process_payment(method: PaymentMethod, amount):
    method.pay(amount)  # Works with any PaymentMethod subclass

# I - Interface Segregation
class Printable(ABC):
    @abstractmethod
    def print(self): pass

class Scannable(ABC):
    @abstractmethod
    def scan(self): pass

# D - Dependency Inversion
class UserService:
    def __init__(self, database: IDatabase):  # Depend on interface
        self.database = database

Q12: What is composition vs inheritance?

Answer:

  • Inheritance - "is-a" relationship (Dog is-an Animal)
  • Composition - "has-a" relationship (Car has-an Engine)

Favor composition over inheritance when:

  • You need flexibility
  • Multiple behaviors need to be combined
  • Avoiding deep hierarchies
# Inheritance
class Animal:
    def eat(self): print("Eating...")

class Dog(Animal):
    def bark(self): print("Woof!")

# Composition
class Engine:
    def start(self): print("Engine started")

class Wheels:
    def rotate(self): print("Wheels rotating")

class Car:
    def __init__(self):
        self.engine = Engine()  # Has-an Engine
        self.wheels = Wheels()  # Has-a Wheels
    
    def drive(self):
        self.engine.start()
        self.wheels.rotate()

car = Car()
car.drive()

Follow-up: "When would you choose inheritance over composition?"

Answer: When there's a clear "is-a" relationship and shared behavior across all subclasses.


Q13: What are static methods and class methods?

Answer:

  • Instance method - Operates on instance (self)
  • Class method - Operates on class (cls)
  • Static method - Utility function, no access to instance or class
class User:
    user_count = 0  # Class variable
    
    def __init__(self, name):
        self.name = name  # Instance variable
        User.user_count += 1
    
    # Instance method
    def greet(self):
        print(f"Hello, I'm {self.name}")
    
    # Class method
    @classmethod
    def get_user_count(cls):
        return cls.user_count
    
    # Static method
    @staticmethod
    def is_valid_username(username):
        return len(username) >= 3

user1 = User("Alice")
user2 = User("Bob")

user1.greet()  # Instance method

print(User.get_user_count())  # Class method: 2

print(User.is_valid_username("Jo"))  # Static method: False

Q14: What are properties (getters/setters)?

Answer:

Properties provide controlled access to attributes with validation.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Getter"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Setter with validation"""
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Computed property"""
        return self._celsius * 9/5 + 32

temp = Temperature(25)

print(temp.celsius)     # Getter: 25
print(temp.fahrenheit)  # Computed: 77.0

temp.celsius = 30       # Setter

try:
    temp.celsius = -300  # Validation error
except ValueError as e:
    print(e)  # Below absolute zero!

Q15: Explain the __init__ and __new__ methods.

Answer:

  • __new__ - Creates the instance (constructor)
  • __init__ - Initializes the instance

Execution order: __new____init__

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("__new__ called (creates instance)")
        instance = super().__new__(cls)
        return instance
    
    def __init__(self, value):
        print("__init__ called (initializes instance)")
        self.value = value

obj = MyClass(42)
# Output:
# __new__ called (creates instance)
# __init__ called (initializes instance)

# Use case for __new__: Singleton pattern
class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # True (same instance)

Part 4: Advanced & Tricky Questions

Q16: What is the difference between __str__ and __repr__?

Answer:

  • __str__ - Human-readable string (for end users)
  • __repr__ - Developer-friendly representation (for debugging)

Rule: repr(obj) should ideally be valid Python code to recreate the object.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
    
    def __str__(self):
        """For print() and str()"""
        return f"{self.title} by {self.author}"
    
    def __repr__(self):
        """For debugging and repr()"""
        return f"Book(title='{self.title}', author='{self.author}')"

book = Book("1984", "George Orwell")

print(str(book))   # 1984 by George Orwell
print(repr(book))  # Book(title='1984', author='George Orwell')

# In interactive shell
book  # Uses __repr__

Q17: What is the Method Resolution Order (MRO)?

Answer:

MRO determines the order in which Python looks for methods in a class hierarchy, especially with multiple inheritance.

C3 Linearization Algorithm is used.

class A:
    def method(self):
        print("A")

class B(A):
    def method(self):
        print("B")

class C(A):
    def method(self):
        print("C")

class D(B, C):
    pass

d = D()
d.method()  # Output: B

# Check MRO
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

# Or use mro() method
print(D.mro())

Q18: What are magic/dunder methods?

Answer:

Magic methods (double underscore methods) define how objects behave with built-in operations.

Common ones:

  • __init__ - Initialization
  • __str__, __repr__ - String representation
  • __len__ - Length
  • __eq__, __lt__, __gt__ - Comparison
  • __add__, __sub__ - Arithmetic
  • __getitem__, __setitem__ - Indexing
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(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 int((self.x**2 + self.y**2) ** 0.5)

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)        # Vector(4, 6) using __add__
print(v1 == v2)       # False using __eq__
print(len(v1))        # 2 using __len__

Q19: What are descriptors?

Answer:

Descriptors are objects that define how attribute access is handled using __get__, __set__, and __delete__ methods.

Use cases: Validation, type checking, lazy loading

class Positive:
    def __init__(self, name):
        self.name = name
    
    def __get__(self, obj, objtype=None):
        return obj.__dict__.get(self.name, 0)
    
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError(f"{self.name} must be positive")
        obj.__dict__[self.name] = value

class Product:
    price = Positive("price")
    quantity = Positive("quantity")
    
    def __init__(self, price, quantity):
        self.price = price
        self.quantity = quantity

product = Product(10, 5)
print(product.price)  # 10

try:
    product.price = -5  # Validation error
except ValueError as e:
    print(e)  # price must be positive

Q20: How do you implement a Singleton pattern in Python?

Answer:

Singleton ensures a class has only one instance.

# Method 1: Using __new__
class Singleton:
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self, value):
        self.value = value

s1 = Singleton(10)
s2 = Singleton(20)

print(s1 is s2)  # True
print(s1.value)  # 20 (both point to same instance)


# Method 2: Using decorator
def singleton(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Database:
    def __init__(self, connection_string):
        self.connection_string = connection_string

db1 = Database("mysql://localhost")
db2 = Database("postgresql://localhost")

print(db1 is db2)  # True

Interview Tips

How to Answer OOP Questions:

  1. Start with definition - Clear, concise explanation
  2. Explain why it matters - Benefits, use cases
  3. Give a code example - Simple, relevant code
  4. Mention trade-offs - When to use, when not to
  5. Follow up with real-world - Where you've used it

Common Follow-ups:

  • "Give a real-world example"
  • "When would you not use this?"
  • "What are the trade-offs?"
  • "How does this work in Python specifically?"

Red Flags to Avoid:

  • ❌ Theoretical answers without examples
  • ❌ Code examples with syntax errors
  • ❌ Saying "I don't know" without trying
  • ❌ Overcomplicating simple concepts

Conclusion

Key preparation steps:

  1. ✅ Understand the four pillars deeply
  2. ✅ Practice coding examples for each concept
  3. ✅ Learn SOLID principles
  4. ✅ Know common design patterns
  5. ✅ Practice explaining concepts out loud

Remember: Interviewers want to see:

  • Clear understanding of concepts
  • Ability to write clean code
  • Real-world application knowledge
  • Problem-solving skills

Practice these questions, understand the concepts, and you'll ace your OOP interviews!


Preparing for coding interviews? I'd love to hear about your journey! Connect with me on Twitter or LinkedIn!

Support My Work

If this guide helped you with this topic, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for developers.

☕ Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!


Cover image by Jakub Żerdzicki on Unsplash

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

© ojaswiat.com 2025-2027