Python OOP interview questions assess how well developers understand classes, objects, inheritance, polymorphism, encapsulation and abstraction. Interviewers may also ask candidates to write short programs, explain method overriding or discuss how object-oriented design helps organise application code.
This guide covers 50 Python object-oriented programming interview questions and answers, ranging from beginner concepts to practical coding scenarios. The explanations and examples focus on Python 3, including its class model, special methods, inheritance behaviour and object-oriented programming conventions.
Before reviewing the interview questions, it is useful to understand the four commonly discussed OOP principles and how Python implements them.
Python OOP Interview Questions: Key Highlights
- 50 interview questions: Python 3 object-oriented programming concepts, code examples and advanced class design topics.
- Core OOP principles: Inheritance, polymorphism, encapsulation and abstraction, explained with practical examples.
- Python fundamentals: Classes, objects, instance attributes, class attributes, methods and object initialisation.
- Intermediate concepts: Properties, method overriding, multiple inheritance, MRO and special methods.
- Advanced topics: Dataclasses, descriptors, Protocols, decorators, object copying and metaclasses.
- Coding practice: Working Python examples with expected output and explanations of common mistakes.
- Interview preparation: Explain how features work, identify their limitations and discuss when particular designs are appropriate.
What Is Object-Oriented Programming in Python?
Object-oriented programming (OOP) is a programming approach that organises related data and behaviour into objects. In Python, classes define how objects are constructed and what attributes and methods they provide.
A class defines a type of object, while an instance is an object created from that class. Instance attributes can store information specific to an object, and methods define behaviour associated with the class.
Python supports object-oriented programming alongside other programming approaches, including procedural and functional styles. Developers can therefore choose classes where grouping state and behaviour makes a program easier to understand or maintain.
The Four Main OOP Principles in Python
The four commonly taught principles of object-oriented programming are inheritance, polymorphism, encapsulation and abstraction. Python supports these principles through classes, methods, attribute conventions and built-in language features.
1. Inheritance in Python
Inheritance allows a class to derive behaviour and attributes from another class. The class being inherited from is called the base class or parent class. The class that inherits is called the derived class or child class.
Inheritance can support code reuse and specialisation. A derived class can use inherited methods, define additional methods or override existing behaviour.
Python inheritance example:
class Employee:
def __init__(self, name):
self.name = name
def describe(self):
return f"Employee: "
class Developer(Employee):
def __init__(self, name, language):
super().__init__(name)
self.language = language
def describe(self):
return f" , language: "
developer = Developer("Asha", "Python")
print(developer.describe())
Output:
Employee: Asha, language: PythonIn this example, Developer inherits from Employee. The derived class uses super() to call the inherited initialisation method and extend the description returned by the parent class.
Interview point: Python also supports multiple inheritance. When methods or attributes are inherited from multiple classes, Python uses a method resolution order (MRO) to determine how they are looked up. This becomes important when explaining super() and cooperative inheritance.
2. Polymorphism in Python
Polymorphism allows different objects to respond to the same operation or method call according to their own behaviour. In Python, this can work through inheritance, method overriding and duck typing.
Objects do not always need to inherit from the same parent class to support the same operation. Python code can often work with any object that provides the expected methods or behaviour.
Python polymorphism example:
class EmailNotification:
def send(self):
return "Email sent"
class SMSNotification:
def send(self):
return "SMS sent"
def notify(channel):
return channel.send()
print(notify(EmailNotification()))
print(notify(SMSNotification()))
Output:
Email sent
SMS sentThe notify() function calls send() on the supplied object. Both notification classes implement that method, so the function can work with either object without checking its specific class.
This is an example of duck typing, where the code relies on the behaviour an object provides rather than requiring a particular inheritance relationship.
Interview point: Polymorphism is not simply defining methods with the same name. The important idea is that different objects can be used through a common operation while providing appropriate behaviour.
3. Encapsulation in Python
Encapsulation involves grouping related data and operations within an object and defining a controlled interface for interacting with that data.
In Python, encapsulation commonly relies on methods, properties and naming conventions. Unlike languages with strictly enforced private and protected access modifiers, Python does not generally prevent outside code from accessing an attribute simply because it has a leading underscore.
By convention, an attribute prefixed with a single underscore is treated as non-public. Names beginning with two underscores in a class definition may undergo name mangling, but this is not a security mechanism.
Python encapsulation example:
class BankAccount:
def __init__(self, balance=0):
if balance < 0:
raise ValueError("Initial balance cannot be negative")
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
account = BankAccount(100)
account.deposit(50)
print(account.balance)
Output:
150The BankAccount class stores its balance in _balance, a non-public attribute by convention. The deposit() method checks the amount before updating it, while the balance property provides a read-only interface through normal attribute access.
Important limitation: This example demonstrates a controlled public interface, not strict access protection. Outside code can still access or modify _balance directly. The validation applies when callers use the provided constructor and deposit() method.
Interview point: Python properties are useful for exposing attribute-like interfaces while implementing validation or calculated values behind them. They should not be confused with security boundaries.
4. Abstraction in Python
Abstraction focuses on exposing the relevant behaviour of an object while separating that behaviour from implementation details. It allows code to work with an agreed interface without depending on every detail of how a particular class performs its task.
Python supports formal abstract base classes through its standard-library abc module. The ABC class and @abstractmethod decorator can define methods that concrete subclasses must implement before their instances can be created.
Python abstraction example:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
rectangle = Rectangle(4, 5)
print(rectangle.area())
Output:
20Shape defines area() as an abstract method. Rectangle provides a concrete implementation, allowing an instance of Rectangle to be created and its area to be calculated.
Attempting to instantiate Shape directly raises a TypeError because it has an unimplemented abstract method.
Interview point: An abstract method in Python can contain an implementation. The important requirement is that a concrete subclass must satisfy the abstract methods before it can be instantiated.
Inheritance vs Polymorphism vs Encapsulation vs Abstraction
| OOP Principle | Main Purpose | Python Example |
|---|---|---|
| Inheritance | Derive and specialise behaviour from an existing class | class Developer(Employee) |
| Polymorphism | Use different objects through a common operation | Different objects implementing send() |
| Encapsulation | Group data and behaviour behind a controlled interface | BankAccount with deposit() and a balance property |
| Abstraction | Specify required behaviour separately from its implementation | ABC and @abstractmethod |
These principles are related but not interchangeable. For example, inheritance can support polymorphism, while abstraction and encapsulation address different aspects of an object’s interface and implementation.
5. What is self in Python, and why is it used?
In Python, self is the conventional name for the first parameter of an instance method. It refers to the instance on which the method is called, allowing the method to access or modify that object’s attributes and call other instance methods.
When an instance method is accessed through an object, Python normally binds that object to the method. This means developers do not need to pass self explicitly when calling the method through an instance.
Example: Using self to access instance attributes
class Employee:
def __init__(self, name, department):
self.name = name
self.department = department
def describe(self):
return f" works in "
first = Employee("Asha", "Engineering")
second = Employee("Ravi", "Finance")
print(first.describe())
print(second.describe())
Output:
Asha works in Engineering
Ravi works in FinanceIn this example, each Employee instance stores its own name and department. The describe() method uses self to retrieve the attributes belonging to the particular instance on which it is called.
Is self a Python keyword? No. The name self is a widely followed convention, not a reserved keyword. A different parameter name is technically possible, but using self makes instance methods easier for other Python developers to recognise and understand.
Interview point: self refers to the current instance, while cls conventionally refers to the class in a method decorated with @classmethod. A static method does not automatically receive either an instance or a class argument.
6. What is __init__() in Python?
__init__() is a special method used to initialise an instance after it has been created. It is automatically called during ordinary class instantiation when the object supports that initialisation method.
Developers commonly use __init__() to assign initial instance attributes and validate the arguments provided when creating an object.
Example:
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
account = Account("Asha", 100)
print(account.balance)Output:
100When Account(“Asha”, 100) is evaluated, Python creates an Account instance and calls its __init__() method with the supplied arguments. The method assigns owner and balance to that instance.
Interview point: __init__() is commonly described as a constructor, but technically it initialises an already-created instance. Instance creation is handled through __new__(). An __init__() method must return None, either explicitly or implicitly.
7. What is __new__() in Python, and how is it different from __init__()?
__new__() is a special method responsible for creating and returning an instance. It receives the class as its first argument, conventionally named cls.
By contrast, __init__() receives an instance that has already been created and initialises its state.
Example: Customising an immutable type
class PositiveInt(int):
def __new__(cls, value):
if value <= 0:
raise ValueError("Value must be positive")
return super().__new__(cls, value)
number = PositiveInt(7)
print(number + 3)Output:
10PositiveInt subclasses the immutable built-in int type. Its __new__() method checks the supplied value before asking the parent class to create the integer instance.
Calling PositiveInt(0) raises a ValueError because the example does not permit zero or negative values.
When is __new__() useful? Developers may override it when customising instance creation, particularly when subclassing immutable built-in types. Most ordinary application classes do not need to override __new__().
Interview point: If __new__() returns an instance of the class being constructed, Python normally proceeds to call __init__(). If it returns an object that is not an instance of that class, that class’s __init__() is not invoked.
8. What is the difference between class attributes and instance attributes?
Class attributes are defined on the class and can be shared by its instances. Instance attributes belong to individual objects and can store different values for each instance.
Example:
class Employee:
company = "Foundit"
def __init__(self, name):
self.name = name
first = Employee("Asha")
second = Employee("Ravi")
Employee.company = "New Company"
print(first.company)
print(second.company)
print(first.name)
print(second.name)Output:
New Company
New Company
Asha
RaviBoth instances access the class attribute company. Updating Employee.company changes the value found through both instances because neither defines its own company attribute.
By contrast, first.name and second.name are instance attributes with separate values.
Common mistake: Assigning first.company = “Other Company” creates an instance attribute that shadows the class attribute for first. It does not update Employee.company.
Mutable class attributes such as lists also require care because instances may share the same object unless an instance-specific attribute is assigned.
9. What are instance methods, class methods and static methods in Python?
Python supports three common method patterns, distinguished by how they receive access to an instance or class:
- Instance method: Receives the instance as its first argument, normally named self.
- Class method: Uses the @classmethod decorator and receives the class as its first argument, normally named cls.
- Static method: Uses the @staticmethod decorator and does not receive an instance or class argument automatically.
Example:
class Report:
format_name = "CSV"
def __init__(self, title):
self.title = title
def label(self):
return f" ( )"
@classmethod
def from_text(cls, text):
return cls(text.strip())
@staticmethod
def is_valid_title(title):
return bool(title.strip())
report = Report.from_text(" Sales ")
print(report.label())
print(Report.is_valid_title(" "))Output:
Sales (CSV)
FalseIn this example, label() is an instance method that accesses the report title. The from_text() class method creates an instance through cls and can serve as an alternative constructor. The is_valid_title() static method performs a validation check without automatically receiving self or cls.
Interview point: Static methods are not the same as methods bound to a class. Class methods are bound to the class, while static methods behave like ordinary functions stored in a class namespace.
10. What is the difference between composition and inheritance in Python?
Inheritance establishes a relationship in which one class derives from another. Composition builds an object using other objects as components.
Inheritance is often appropriate when the derived class is a specialised form of the base class. Composition is useful when one object contains or uses another object to perform part of its behaviour.
Example: Composition
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self, engine):
self.engine = engine
def drive(self):
return self.engine.start() + "; car ready"
car = Car(Engine())
print(car.drive())Output:
Engine started; car readyCar contains a reference to an Engine object and delegates the starting operation to it. Car does not inherit from Engine because a car is not a type of engine.
When should you use composition? Consider composition when an object needs to use another object’s functionality without becoming a subtype of it. Composition can also make replacing or testing individual dependencies easier.
Interview point: Inheritance and composition are complementary design tools. Choosing between them depends on the intended relationships, shared behaviour and maintenance requirements.
Python Attributes, Properties and Method Overriding
These Python OOP interview questions explore how objects expose attributes, validate changes, override inherited methods and define their own behaviour for built-in operations.
11. What is the @property decorator in Python?
The @property decorator allows a method to be accessed using attribute syntax. It is useful when an object needs to expose a value while controlling how that value is retrieved or updated.
A property can define a getter, an optional setter and an optional deleter. The @property_name.setter decorator is commonly used to validate assignments.
Example: Validating a product price
class Product:
def __init__(self, price):
self.price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
product = Product(100)
product.price = 125
print(product.price)Output:
125When product.price is assigned a value, Python calls the property’s setter. The setter rejects negative values and stores valid values in _price. Accessing product.price calls the getter.
Interview point: Properties can preserve a simple attribute interface while allowing validation or calculated values. They do not make the underlying attribute inaccessible or create a security boundary.
12. How do getattr(), setattr(), hasattr() and delattr() work in Python?
Python provides built-in functions for accessing and modifying attributes dynamically. These are useful when the attribute name is determined while a program is running.
- getattr(object, name): Retrieves a named attribute. An optional default can be supplied if it does not exist.
- setattr(object, name, value): Assigns a value to a named attribute.
- hasattr(object, name): Checks whether attribute lookup succeeds without raising AttributeError.
- delattr(object, name): Deletes a named attribute where deletion is permitted.
Example:
class User:
def __init__(self, username):
self.username = username
user = User("asha")
print(getattr(user, "username"))
print(hasattr(user, "email"))
setattr(user, "email", "a@example.com")
print(user.email)
delattr(user, "email")
print(hasattr(user, "email"))Output:
asha
False
a@example.com
FalseThe example reads the existing username, checks for email, creates the email attribute and removes it.
Interview point: Dynamic attribute operations follow Python’s normal attribute-access rules, including properties and customised attribute methods. They do not bypass validation automatically.
13. What is method overriding in Python?
Method overriding occurs when a derived class defines a method with the same name as a method inherited from its base class. The derived class can provide specialised behaviour for that method.
Example:
class Animal:
def speak(self):
return "Unknown sound"
class Dog(Animal):
def speak(self):
return "Woof"
print(Animal().speak())
print(Dog().speak())Output:
Unknown sound
WoofDog inherits from Animal but provides its own implementation of speak(). Calling speak() on a Dog instance therefore uses the derived class’s implementation.
Interview point: A derived class can also extend inherited behaviour by calling super(). Python resolves inherited methods according to the method resolution order (MRO), which becomes particularly important with multiple inheritance.
14. What is the difference between object identity and equality in Python?
Object identity concerns whether two references point to the same object. Equality concerns whether two values are considered equal according to their comparison behaviour.
istests whether two references identify the same object.==evaluates equality using the relevant comparison behaviour, commonly involving __eq__().
Example: Custom equality for a Point class
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
first = Point(2, 3)
second = Point(2, 3)
print(first == second)
print(first is second)Output:
True
FalseBoth Point objects contain the same coordinate values, so the custom __eq__() method considers them equal. However, they are separate instances, so the identity comparison returns False.
Why return NotImplemented? When the other operand is unsupported, returning NotImplemented allows Python’s comparison machinery to consider the other operand’s implementation where appropriate.
Interview point: Defining __eq__() can also affect hashability. For ordinary classes that override equality without defining an appropriate __hash__(), Python generally makes instances unhashable to avoid inconsistent hashing behaviour.
15. What is the difference between __str__() and __repr__() in Python?
__str__() and __repr__() are special methods that define string representations of objects.
- __str__(): Provides a readable representation, commonly used by print() and str().
- __repr__(): Provides an unambiguous representation, commonly useful for debugging and inspection. Where practical, it may resemble an expression that recreates the object.
Example:
class Book:
def __init__(self, title):
self.title = title
def __str__(self):
return self.title
def __repr__(self):
return f"Book( )"
book = Book("Python Basics")
print(str(book))
print(repr(book))Output:
Python Basics
Book('Python Basics')The __str__() implementation returns the book title for readable display. The __repr__() implementation includes the class name and a representation of the title.
Interview point: If a class defines __repr__() but not __str__(), the inherited default __str__() normally uses the __repr__() result.
Python Special Methods and Object Behaviour
16. How do __len__() and __contains__() work in Python classes?
Python’s special methods, sometimes called dunder methods, allow objects to support built-in operations. For example, __len__() defines behaviour for len(), while __contains__() can define membership checks using the in operator.
Example: A custom collection
class Basket:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __contains__(self, item):
return item in self.items
basket = Basket(["apple", "pear"])
print(len(basket))
print("apple" in basket)Output:
2
TrueThe len() function calls the object’s length behaviour, and the membership test checks whether apple exists in the basket’s items.
Interview point: Special methods allow custom objects to work with familiar Python syntax. When __contains__() is absent, Python may use iteration or sequence-style fallback behaviour for membership testing.
17. What is the difference between __getattr__() and __getattribute__()?
Both methods can customise how Python accesses attributes, but they are invoked under different conditions.
- __getattribute__(): Participates in ordinary attribute lookup whenever an attribute is accessed on an instance.
- __getattr__(): Provides fallback behaviour when ordinary attribute lookup raises AttributeError.
Example: Providing a default value for a missing attribute
class Settings:
def __init__(self):
self.theme = "dark"
def __getattr__(self, name):
if name == "language":
return "en"
raise AttributeError(f"No attribute: ")
settings = Settings()
print(settings.theme)
print(settings.language)Output:
dark
enAccessing theme uses the normal attribute lookup process. Since language does not exist as an ordinary attribute, Python invokes __getattr__(), which returns en.
Interview point: Overriding __getattribute__() requires care because accessing attributes within it can cause infinite recursion. Implementations normally delegate to object.__getattribute__() for ordinary lookup.
18. How can Python classes protect object state from invalid updates?
One purpose of encapsulation is to provide an interface that maintains valid object state. Instead of expecting callers to modify internal attributes directly, a class can expose methods that check requested changes before applying them.
Example: Validating deposits in a bank account
class Account:
def __init__(self, balance):
if balance < 0:
raise ValueError("Balance cannot be negative")
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
account = Account(50)
account.deposit(25)
print(account.balance)Output:
75The constructor rejects a negative initial balance, and deposit() rejects zero or negative deposit amounts. The balance property allows callers to read the current balance without offering a corresponding public property setter.
Important limitation: The leading underscore signals that _balance is non-public by convention. It does not prevent external code from accessing or modifying the attribute. The example protects state through its intended public interface rather than enforcing strict privacy.
Interview point: Explain the invariant a class is trying to maintain, such as preventing an invalid balance. Then show how its public operations validate changes.
19. What is __slots__ in Python, and when should you use it?
__slots__ allows a class to declare specific instance attributes. In suitable classes, this can avoid allocating a separate instance __dict__ and reduce memory overhead.
Example:
class User:
__slots__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Asha", 25)
print(user.name)
print(user.age)
print(hasattr(user, "__dict__"))Output:
Asha
25
FalseThe User class declares two instance attributes. For this class, instances do not have an ordinary __dict__, and assigning an undeclared attribute such as user.email would raise AttributeError.
Important limitations: The effect of __slots__ depends on inheritance and class design. For example, a subclass without appropriate slots may introduce an instance dictionary. Memory savings are also workload-dependent.
Interview point: Use __slots__ when attribute restrictions or memory characteristics justify the trade-offs, rather than assuming every Python class should define it.
20. What are computed properties in Python?
A computed property exposes a value through attribute syntax even though the value is calculated when accessed. Python’s @property decorator can implement this without storing a separate copy of the result.
Example: Converting Celsius to Fahrenheit
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
def __repr__(self):
return f"Temperature( )"
temperature = Temperature(20)
print(temperature.fahrenheit)
print(repr(temperature))Output:
68.0
Temperature(20)The fahrenheit property calculates a temperature conversion using the current celsius attribute. The result is not stored independently, so changing temperature.celsius changes the value returned by temperature.fahrenheit.
Interview point: Computed properties are useful for inexpensive derived values that naturally behave like attributes. Expensive calculations or operations with substantial side effects may be clearer as ordinary methods.
Python Encapsulation, Abstraction and Inheritance Questions
21. Does Python have public, protected and private access modifiers?
Python does not enforce public, protected and private access modifiers in the same way as Java or C++. Instead, it relies on attribute naming conventions and name mangling.
- Public: Attributes such as name are intended for ordinary external access.
- Non-public: A single leading underscore, such as _department, indicates an internal implementation detail by convention.
- Name-mangled: A name such as __employee_id undergoes name mangling within a class definition, primarily to avoid accidental clashes in subclasses.
Example:
class Employee:
def __init__(self, name):
self.name = name
self._department = "Engineering"
self.__employee_id = 42
def get_id(self):
return self.__employee_id
employee = Employee("Asha")
print(employee.name)
print(employee._department)
print(employee.get_id())
print(hasattr(employee, "__employee_id"))Output:
Asha
Engineering
42
FalseThe public and conventionally non-public attributes can both be accessed directly. The double-underscore attribute is name-mangled, so lookup using its original spelling does not find it.
Interview point: Python’s naming conventions do not provide strict privacy. Name mangling is intended to reduce accidental name clashes, not to protect sensitive information.
22. What is an abstract base class (ABC) in Python?
An abstract base class defines shared behaviour or requirements that concrete subclasses must satisfy. Python provides the ABC class and @abstractmethod decorator through the standard-library abc module.
Example:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CardPayment(PaymentMethod):
def pay(self, amount):
return f"Paid by card"
print(CardPayment().pay(250))
print(bool(PaymentMethod.__abstractmethods__))Output:
Paid 250 by card
TruePaymentMethod defines an abstract pay() method. CardPayment implements that method and can therefore be instantiated. PaymentMethod itself cannot be instantiated while the abstract requirement remains unimplemented.
Interview point: Abstract base classes may also contain ordinary concrete methods. Not every method in an ABC must be abstract.
23. Can abstract methods contain implementations in Python?
Yes. Python permits methods decorated with @abstractmethod to contain executable code. Concrete subclasses must satisfy the abstract method requirement but may call the inherited implementation through super().
Example:
from abc import ABC, abstractmethod
class Greeter(ABC):
@abstractmethod
def greet(self):
return "Hello"
class FriendlyGreeter(Greeter):
def greet(self):
return super().greet() + ", Asha"
print(FriendlyGreeter().greet())Output:
Hello, AshaThe base class provides an implementation while marking greet() as abstract. FriendlyGreeter overrides it and extends its behaviour.
Interview point: An abstract method is not necessarily an empty method. The abc module supports abstract methods with implementations, including cooperative calls through super().
24. What does super() do in Python?
The super() function provides a proxy that delegates attribute lookup according to Python’s method resolution order (MRO). It is commonly used to call inherited methods.
It does not create a temporary parent-class object. In multiple inheritance, super() may resolve a method from a class other than the immediate syntactic parent.
Example:
class Person:
def __init__(self, name):
self.name = name
def description(self):
return f"Person: "
class Engineer(Person):
def description(self):
return super().description() + " (Engineer)"
print(Engineer("Asha").description())Output:
Person: Asha (Engineer)Engineer overrides description() but delegates to the inherited method before extending the returned text.
Interview point: super() follows the MRO. Understanding this distinction is especially important for multiple inheritance and cooperative method calls.
25. Does Python support multiple inheritance?
Yes. Multiple inheritance allows a class to inherit from more than one base class. The derived class may access behaviour supplied by each base, subject to Python’s attribute lookup rules.
Example:
class Writer:
def write(self):
return "Writing"
class Reviewer:
def review(self):
return "Reviewing"
class Editor(Writer, Reviewer):
pass
editor = Editor()
print(editor.write())
print(editor.review())Output:
Writing
ReviewingEditor inherits methods from both Writer and Reviewer. Because the parent classes provide different methods, there is no method-name conflict in this example.
Interview point: Multiple inheritance becomes more complex when several base classes share method names or common ancestors. Python’s MRO determines how those methods are resolved.
Python MRO, Iterators and Special Methods
26. What is the Method Resolution Order (MRO) in Python?
The Method Resolution Order determines the order in which Python searches classes for inherited attributes and methods. Python uses C3 linearisation to establish a consistent order in modern class hierarchies.
Example: Diamond inheritance
class Root:
def action(self):
return ["Root"]
class Left(Root):
def action(self):
return ["Left"] + super().action()
class Right(Root):
def action(self):
return ["Right"] + super().action()
class Child(Left, Right):
pass
print(" > ".join(cls.__name__ for cls in Child.__mro__))
print(" > ".join(Child().action()))Output:
Child > Left > Right > Root > object
Left > Right > RootChild’s MRO includes Left, Right and the shared base Root. When Left calls super().action(), Python continues to Right before reaching Root.
Interview point: Inspect the MRO with ClassName.__mro__ or ClassName.mro(). super() follows this order rather than always calling a fixed parent class.
27. What is the difference between isinstance() and issubclass()?
The isinstance() function checks whether an object is an instance of a specified class or compatible subclass. The issubclass() function checks whether one class is a subclass of another, including itself.
Example:
class Vehicle:
pass
class Car(Vehicle):
pass
car = Car()
print(isinstance(car, Car))
print(isinstance(car, Vehicle))
print(issubclass(Car, Vehicle))
print(type(car) is Vehicle)Output:
True
True
True
FalseThe Car object is an instance of both Car and its base class Vehicle for isinstance() checks. Car is a subclass of Vehicle, but the object’s exact type is Car.
Interview point: These functions recognise inheritance and certain virtual subclass relationships. An exact type comparison using type(object) is ClassName has different semantics.
28. How do you create a custom iterator class in Python?
An iterator provides successive values through the iterator protocol. Its __iter__() method returns the iterator, and __next__() returns the next value or raises StopIteration when exhausted.
Example: Countdown iterator
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3)))Output:
[3, 2, 1]Countdown tracks its current value and returns each positive integer until StopIteration ends the sequence.
Interview point: Countdown is its own iterator and becomes exhausted after use. A reusable iterable can instead return a new iterator every time __iter__() is called.
29. How are generators used in object-oriented Python?
A generator produces values lazily using yield. A class can implement __iter__() as a generator method so its instances can be used directly in loops and other iteration contexts.
Example:
class RangeMaker:
def __init__(self, stop):
self.stop = stop
def __iter__(self):
for number in range(self.stop):
yield number * number
numbers = RangeMaker(4)
print(list(numbers))
print(list(numbers))Output:
[0, 1, 4, 9]
[0, 1, 4, 9]Every call to iter(numbers) creates a new generator, allowing the RangeMaker instance to be iterated more than once.
Interview point: A generator automatically preserves execution state between yields. Unlike a manually implemented iterator class, it does not require developers to implement __next__() themselves.
30. What is operator overloading in Python?
Operator overloading allows custom objects to define behaviour for operations such as addition, subtraction and equality comparison through special methods.
For example, __add__() defines how an object responds to the + operator for supported operands.
Example:
class Price:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
if not isinstance(other, Price):
return NotImplemented
return Price(self.amount + other.amount)
def __repr__(self):
return f"Price( )"
print(Price(40) + Price(60))Output:
Price(100)The __add__() method returns a new Price object whose amount equals the sum of the two operands.
Interview point: Returning NotImplemented for unsupported operand types lets Python consider an appropriate reflected operation. Operator behaviour should be predictable and consistent with the class’s purpose.
Intermediate Python OOP Interview Questions
These intermediate Python OOP questions examine how developers build maintainable classes, validate object state and customise attribute behaviour. The examples use standard Python 3 features rather than external packages.
31. What is a dataclass in Python, and when should you use one?
A dataclass is a class decorated with @dataclass from Python’s standard-library dataclasses module. It can automatically generate methods such as __init__(), __repr__() and __eq__() based on declared fields.
Dataclasses are particularly useful for classes whose main purpose is storing related values, reducing the need to write repetitive initialisation and representation methods.
Example:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
level: int = 1
first = Employee("Asha")
second = Employee("Asha")
print(first)
print(first == second)Output:
Employee(name='Asha', level=1)
TrueThe dataclass decorator generates an initialiser accepting name and level, a readable representation and an equality method that compares corresponding fields when the instances are of the same class.
Interview point: Dataclasses do not automatically make objects immutable. The default dataclass is mutable. Developers can use options such as frozen=True where appropriate, although frozen dataclasses do not guarantee deep immutability.
32. Why should dataclasses use default_factory for mutable fields?
Mutable values, such as lists and dictionaries, should normally be created separately for each instance when they represent instance-specific state.
In a dataclass, field(default_factory=...) accepts a zero-argument callable that creates a fresh default value when a new instance is initialised without that field being supplied.
Example:
from dataclasses import dataclass, field
@dataclass
class Project:
tasks: list[str] = field(default_factory=list)
first = Project()
second = Project()
first.tasks.append("Review")
print(first.tasks)
print(second.tasks)Output:
['Review']
[]Each Project instance receives its own list. Adding a task to first does not change the list stored by second.
Common mistake: Defining a mutable class attribute and assuming every instance has its own independent copy. Without instance-specific initialisation, objects may share and modify the same list.
Interview point: Modern dataclasses reject many unhashable mutable default values and direct developers towards default_factory. This helps prevent accidental sharing of mutable instance state.
33. What is __post_init__() in a Python dataclass?
The __post_init__() method allows a dataclass to perform additional initialisation after its generated __init__() method assigns the declared fields.
It is useful for validating field values or calculating information that depends on those fields.
Example: Validating a discount
from dataclasses import dataclass
@dataclass
class Discount:
percent: int
def __post_init__(self):
if not 0 <= self.percent <= 100:
raise ValueError("Percent must be 0 to 100")
discount = Discount(20)
print(discount.percent)Output:
20When Discount(20) is constructed, its generated __init__() assigns the percent field and then invokes __post_init__(). The validation accepts values from zero through 100.
Constructing Discount(120) instead raises ValueError.
Important limitation: This validation runs during initialisation. In a mutable dataclass, directly assigning discount.percent = 120 afterwards is not prevented by this example. Further controls are needed if an invariant must be enforced throughout the object’s lifetime.
Interview point: __post_init__() is automatically invoked by the generated dataclass __init__(). If a developer supplies a custom __init__() and no generated initialiser is used, it is not automatically called.
34. What are descriptors in Python?
A descriptor is an object that customises attribute access through methods such as __get__(), __set__() or __delete__(). Descriptors form part of Python’s attribute lookup machinery and are used by features including properties, methods and certain other class behaviours.
A descriptor can centralise validation logic that would otherwise be repeated across attributes or classes.
Example: Reusable positive-number validation
class PositiveNumber:
def __set_name__(self, owner, name):
self.storage_name = "_" + name
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("Must be positive")
setattr(instance, self.storage_name, value)
class Package:
weight = PositiveNumber()
def __init__(self, weight):
self.weight = weight
package = Package(5)
print(package.weight)Output:
5PositiveNumber is a data descriptor because it defines __set__(). When weight is assigned, the descriptor validates the value and stores it in _weight on the instance.
The __set_name__() method receives information about the name under which the descriptor was assigned during class creation.
Interview point: Descriptors are useful for reusable attribute-management behaviour. They are more advanced than properties and should be introduced when that additional flexibility is justified.
35. How do context managers work in Python classes?
A context manager is an object that manages entry into and exit from a block of code using Python’s with statement.
A class-based context manager implements __enter__() and __exit__(). This is useful when a task requires setup and reliable cleanup, such as handling files, locks or other resources.
Example:
class Session:
def __enter__(self):
print("Session opened")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Session closed")
return False
with Session() as session:
print("Work completed")Output:
Session opened
Work completed
Session closed__enter__() runs when execution enters the with block. __exit__() runs when execution leaves it, including when an exception occurs inside the block after successful entry.
Returning False from __exit__() means that any exception raised inside the block is not suppressed by the context manager.
Interview point: Context managers help separate resource lifecycle management from the operations performed while the resource is available.
Python Class Design and Reusable Behaviour
36. What is the __call__() method in Python?
The __call__() special method allows an instance of a custom class to be called using function-call syntax.
Callable objects are useful when an operation needs to retain configuration or state between calls. They are sometimes used for validators, processing rules and configurable transformations.
Example: Callable multiplier
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
triple = Multiplier(3)
print(triple(4))Output:
12When triple(4) is evaluated, Python calls the __call__() method on the Multiplier instance. The object retains its factor attribute between calls.
Interview point: Callable objects combine state and function-like behaviour. Python’s callable() built-in can be used to check whether an object appears callable.
37. What is dependency injection in Python OOP?
Dependency injection is a design technique in which an object receives another object or service it depends on, rather than constructing that dependency internally.
This can reduce coupling and make dependencies easier to replace or test.
Example:
class ConsoleSender:
def send(self, message):
return "Sent: " + message
class AlertService:
def __init__(self, sender):
self.sender = sender
def alert(self, message):
return self.sender.send(message)
service = AlertService(ConsoleSender())
print(service.alert("Build passed"))Output:
Sent: Build passedAlertService receives its sender dependency through the constructor. It does not need to know how ConsoleSender is created or how the sending operation is implemented.
For testing, a different sender object implementing the expected send() behaviour could be supplied.
Interview point: Dependency injection does not require a dedicated framework. In Python, passing an object through a constructor, method or function can be sufficient.
38. What is a Protocol in Python, and how does it support duck typing?
The Protocol class from the typing module allows developers to describe an expected interface through structural typing.
A class can satisfy a Protocol by providing the required methods and attributes without explicitly inheriting from that Protocol. This is often called static duck typing.
Example:
from typing import Protocol
class Sender(Protocol):
def send(self, message: str) -> str:
...
class EmailSender:
def send(self, message: str) -> str:
return "Email: " + message
def notify(sender: Sender) -> str:
return sender.send("Ready")
print(notify(EmailSender()))Output:
Email: ReadyEmailSender does not inherit from Sender, but its send() method matches the interface described by the Protocol. A compatible static type checker can recognise this structural relationship.
Important limitation: Type annotations and Protocols do not automatically enforce method signatures at runtime. A Protocol marked with @runtime_checkable can support certain runtime checks, but those checks do not fully validate method signatures or attribute types.
Interview point: Protocols are useful for declaring interfaces without requiring implementation classes to share an explicit base class.
39. What is a mixin class in Python?
A mixin is a class intended to provide reusable behaviour to other classes through inheritance. Unlike a standalone domain class, a mixin usually contributes a specific capability rather than representing a complete business entity.
Mixins can be useful for shared formatting, serialisation or logging behaviour, provided their dependencies and method names are clearly defined.
Example: Reusing a label method
class LabelMixin:
def label(self):
return f" (active)"
class Customer(LabelMixin):
def __init__(self, name):
self.name = name
customer = Customer("Asha")
print(customer.label())Output:
Asha (active)LabelMixin supplies label(), while Customer supplies the name attribute that the method expects.
Important consideration: A mixin may depend on attributes or methods supplied by another class. Poorly designed mixins can make inheritance hierarchies difficult to understand, especially when method names conflict.
Interview point: Use mixins for a focused reusable capability. Use composition when an object should contain or delegate to another object rather than inherit its behaviour.
40. How do you create custom exceptions in Python classes?
Python allows developers to define custom exception classes by inheriting from Exception or a suitable existing exception type.
Custom exceptions help distinguish application-specific failures from unrelated programming or system errors. They can also make error handling more precise.
Example: Invalid order quantity
class InvalidOrderError(Exception):
pass
class Order:
def __init__(self, quantity):
if quantity <= 0:
raise InvalidOrderError(
"Quantity must be positive"
)
self.quantity = quantity
try:
order = Order(0)
except InvalidOrderError as error:
print(str(error))Output:
Quantity must be positiveThe Order class raises a domain-specific exception when the supplied quantity is invalid. The caller catches InvalidOrderError rather than catching every possible exception.
Interview point: Most application-defined exceptions should derive from Exception rather than BaseException. Avoid catching exceptions too broadly, because doing so can conceal unexpected programming errors.
Advanced Python OOP Interview Questions
Advanced Python object-oriented programming interviews may examine how custom classes interact with built-in operations, how objects are copied, how subclasses are created and how reusable behaviour is organised. The following questions include practical Python 3 examples.
41. How do object equality and hashing work in Python?
Python objects can define equality using __eq__() and hash behaviour using __hash__(). Hashing is important when an object is used as a dictionary key or stored in a set.
Two objects that compare equal must have the same hash value. The attributes used to calculate an object’s hash should remain stable while it is used in a hash-based collection.
Example: A hashable dataclass
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
x: int
y: int
first = Coordinate(2, 3)
second = Coordinate(2, 3)
print(first == second)
print(len( ))Output:
True
1The two Coordinate instances compare equal because their field values match. The frozen dataclass generates suitable equality and hashing methods for these integer fields, so the set contains only one distinct value.
Important: frozen=True prevents normal field reassignment but does not guarantee deep immutability of values stored inside an object. Hashability also depends on the values being hashed.
Interview point: If a regular class defines __eq__() without defining a compatible __hash__(), Python normally makes its instances unhashable. Avoid hashing mutable state that affects equality.
42. How can Python objects support ordering comparisons?
Python classes can define ordering operations through special methods such as __lt__(), __le__(), __gt__() and __ge__(). These determine how custom objects respond to comparisons such as < and >=.
The functools.total_ordering decorator can generate the remaining ordering methods when a class defines __eq__() and at least one ordering method.
Example: Comparing scores
from functools import total_ordering
@total_ordering
class Score:
def __init__(self, points):
self.points = points
def __eq__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.points == other.points
def __lt__(self, other):
if not isinstance(other, Score):
return NotImplemented
return self.points < other.points
print(Score(10) < Score(20))
print(Score(20) >= Score(10))Output:
True
TrueScore defines equality and less-than comparison. The total_ordering decorator supplies the other ordering methods using the provided operations.
Interview point: total_ordering reduces repetitive code, but explicitly implementing all comparison methods may offer simpler stack traces or better performance in some cases. Use NotImplemented when the other operand type is unsupported.
43. How does __getitem__() support indexing and slicing?
The __getitem__() special method allows a custom object to respond to subscription syntax using square brackets. It can receive an integer index, a slice object or another supported key type.
Example: A custom playlist
class Playlist:
def __init__(self, songs):
self._songs = list(songs)
def __getitem__(self, index):
return self._songs[index]
playlist = Playlist(["Intro", "Bridge", "Outro"])
print(playlist[0])
print(playlist[1:])Output:
Intro
['Bridge', 'Outro']When playlist[0] is evaluated, Python calls the relevant __getitem__() implementation with index 0. For playlist[1:], Python passes a slice object, which the underlying list understands.
Interview point: Implementing __getitem__() does not automatically mean a class behaves exactly like a built-in list. Additional methods may be needed for assignment, deletion, length or other collection operations.
44. How does __setitem__() work in custom Python classes?
The __setitem__() special method defines how an object responds to an assignment using square brackets, such as settings[“theme”] = “dark”.
It can be used to validate values before storing them or to implement a custom mapping-like interface.
Example: Validated settings
class Settings:
def __init__(self):
self._values =
def __getitem__(self, key):
return self._values[key]
def __setitem__(self, key, value):
if not isinstance(value, str):
raise TypeError("Value must be text")
self._values[key] = value
settings = Settings()
settings["theme"] = "dark"
print(settings["theme"])Output:
darkSettings stores values in an internal dictionary. The __setitem__() method checks that assigned values are strings before storing them, while __getitem__() retrieves a value using bracket syntax.
Assigning a non-string value, such as settings[“theme”] = 10, raises TypeError.
Interview point: Custom item access is useful when an object needs dictionary-like behaviour with domain-specific validation. For a complete mutable mapping interface, Python also provides abstractions such as collections.abc.MutableMapping.
45. What is __init_subclass__() in Python?
The __init_subclass__() method is a hook that runs when a class is subclassed. It allows a base class to customise or inspect newly defined subclasses.
It can support lightweight registration systems, subclass validation and other class-definition workflows.
Example: Registering plugin subclasses
class Plugin:
registry =
def __init_subclass__(cls, name, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[name] = cls
class CSVPlugin(Plugin, name="csv"):
def export(self):
return "CSV ready"
print(Plugin.registry["csv"]().export())Output:
CSV readyWhen CSVPlugin is defined, the inherited __init_subclass__() hook runs and registers the subclass under the name csv. The registered class can then be instantiated and used.
Interview point: __init_subclass__() customises subclass creation without necessarily requiring a custom metaclass. In cooperative inheritance, handling and forwarding supported keyword arguments correctly is important.
Advanced Python Class Design and Coding Questions
46. What is singledispatchmethod in Python?
singledispatchmethod is a decorator from the functools module that supports different method implementations based on the type of the first non-self or non-cls argument.
This provides a form of type-based method dispatch. It is useful when an operation needs different behaviour for different supported input types.
Example: Formatting different input types
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return f"Other: "
@format.register
def _(self, value: int):
return f"Number: "
@format.register
def _(self, value: str):
return f"Text: "
formatter = Formatter()
print(formatter.format(5))
print(formatter.format("hello"))Output:
Number: 5
Text: helloThe decorator selects the registered implementation according to the runtime type of the value argument. The example handles integers and strings separately.
Important distinction: Python does not automatically overload methods by argument count or signature in the manner of some statically typed languages. Defining another method with the same name in a class ordinarily replaces the earlier definition in that class namespace.
Interview point: singledispatchmethod dispatches on one argument’s type, rather than selecting implementations using all parameter types. It is available in Python 3.8 and later.
47. What is cached_property in Python?
The cached_property decorator from functools allows a calculated value to be stored on an instance after its first successful access. Later accesses can reuse the stored result rather than running the calculation again.
It is useful when a calculation is relatively expensive and the underlying information does not change frequently.
Example: Caching a report total
from functools import cached_property
class Report:
def __init__(self, amounts):
self.amounts = tuple(amounts)
self.calls = 0
@cached_property
def total(self):
self.calls += 1
return sum(self.amounts)
report = Report([10, 20, 30])
print(report.total)
print(report.total)
print(report.calls)Output:
60
60
1The first access to total calculates the sum and stores the result. The second access reuses that cached value, so the calculation counter remains at one.
Important limitations: cached_property usually requires a writable instance __dict__. It does not automatically detect changes to the underlying data. The cached value can be cleared by deleting the attribute.
Under concurrent access, the calculation may run more than once before a value is cached. Applications needing stronger synchronisation must implement their own coordination.
Interview point: Unlike a regular @property, cached_property stores its result as an ordinary instance attribute. It is most suitable when the computed value remains valid for the intended caching period.
48. What is the difference between shallow copy and deep copy in Python OOP?
Copying an object creates another object from an existing one, but the treatment of nested objects depends on the copying method.
- Shallow copy: Creates a new outer object while retaining references to nested objects.
- Deep copy: Recursively copies the contained objects where supported, creating independent nested structures in common cases.
Example:
import copy
class Folder:
def __init__(self, items):
self.items = list(items)
original = Folder(["draft"])
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original.items.append("final")
print(shallow.items)
print(deep.items)Output:
['draft', 'final']
['draft']The shallow copy and original Folder reference the same nested list, so both observe the appended item. The deep copy contains a separately copied list and remains unchanged.
Interview point: Deep copying is not always necessary or desirable. It can be more expensive, and certain objects such as external resources may require special handling. Classes can customise copying through methods such as __copy__() and __deepcopy__().
49. How do you implement the Strategy design pattern in Python?
The Strategy pattern separates interchangeable behaviour from the object that uses it. Instead of embedding every variation in one class, the application supplies an object implementing the required operation.
In Python, this pattern can be implemented using composition and duck typing without requiring a complex inheritance hierarchy.
Example: Different discount strategies
class PercentageDiscount:
def apply(self, amount):
return amount * 0.9
class FixedDiscount:
def apply(self, amount):
return amount - 10
class Checkout:
def __init__(self, discount):
self.discount = discount
def total(self, amount):
return self.discount.apply(amount)
print(Checkout(PercentageDiscount()).total(100))
print(Checkout(FixedDiscount()).total(100))Output:
90.0
90Checkout delegates discount calculation to the supplied object. Both discount classes implement apply(), allowing the application to change the calculation without modifying Checkout.
Important: This is a simplified pattern demonstration. Production pricing requires appropriate decimal handling, currency rules, minimum-price constraints and validation.
Interview point: The Strategy pattern is useful when alternative behaviours need to be independently implemented or tested. For very simple cases, passing a function can be more straightforward than creating a separate class.
50. What is a metaclass in Python, and when would you use one?
A metaclass controls aspects of how Python class objects are created. Most ordinary Python classes use type as their metaclass.
Just as an ordinary class can be used to create instances, a metaclass participates in constructing class objects. Custom metaclasses can inspect or modify class definitions during their creation.
Example: Adding a class attribute during creation
class TaggedType(type):
def __new__(mcls, name, bases, namespace):
namespace["category"] = "registered"
return super().__new__(
mcls, name, bases, namespace
)
class Task(metaclass=TaggedType):
pass
print(Task.category)
print(type(Task).__name__)Output:
registered
TaggedTypeTaggedType is a custom metaclass derived from type. Its __new__() method adds a category attribute to the class namespace before the Task class object is created.
The expression type(Task) returns TaggedType, demonstrating that Task is an instance of the custom metaclass.
When are metaclasses useful? They may be appropriate for frameworks, registries or class-definition validation that require control over class construction.
Interview point: Custom metaclasses add complexity and can create compatibility problems in multiple-inheritance hierarchies. Prefer simpler mechanisms such as class decorators or __init_subclass__() when those satisfy the requirement.
Official references: Consult the Python Data Model, the functools documentation and the copy module documentation for further details on class creation, special methods, caching and object copying.
How to Prepare for Python OOP Interviews
Preparing for a Python OOP interview requires more than memorising definitions. Candidates should be able to explain how classes and objects behave, write valid Python 3 code and justify when inheritance, composition, abstraction or other design techniques are appropriate.
Use the 50 questions in this guide to practise three skills: explaining a concept accurately, demonstrating it with a working example and discussing its limitations or alternatives.
For example, you should be able to distinguish __new__() from __init__(), explain why super() follows the method resolution order, and identify when a property, dataclass or ordinary method is appropriate.
Practise running the examples and modifying their inputs to see how they behave. For version-sensitive features, refer to the official Python 3 documentation.
FAQs
The four main principles of object-oriented programming in Python are inheritance, polymorphism, encapsulation and abstraction. Inheritance allows classes to reuse and extend behaviour, polymorphism supports common operations across different objects, encapsulation organises data and methods behind an interface, and abstraction separates required behaviour from implementation details.
No. Python is a multi-paradigm programming language that supports object-oriented, procedural and functional programming. Although Python represents values such as integers, strings and functions as objects, developers are not required to organise every program around classes.
Freshers should understand classes, objects, self, __init__(), instance and class attributes, inheritance, method overriding and the four OOP principles. They should also practise short Python programs and explain how methods, attributes and object creation work.
Experienced developers may be asked about multiple inheritance, method resolution order (MRO), super(), descriptors, dataclasses, Protocols, special methods, object copying and metaclasses. Interviewers may also evaluate how candidates choose between inheritance and composition and design maintainable classes.
Start by revising core OOP concepts, then practise writing Python 3 classes that demonstrate inheritance, polymorphism, properties, abstract base classes and method overriding. Test your code, understand its output and prepare to explain design decisions, edge cases and limitations. Use the official Python documentation to verify unfamiliar language features.


