Python Object-Oriented Programming
Python Object-Oriented Programming Interview with follow-up questions
1. Can you explain the concept of object-oriented programming in Python?
Object-oriented programming (OOP) is a paradigm that organizes code around objects — instances of classes that bundle state (attributes) with behavior (methods). A class is a blueprint; an object is a concrete instance with its own data.
class Account:
def __init__(self, owner: str, balance: float = 0.0) -> None:
self.owner = owner # instance attribute
self.balance = balance
def deposit(self, amount: float) -> None:
self.balance += amount
Interviewers expect you to name the four pillars and show you understand Python's take on each:
- Encapsulation — bundling data with methods. Python has no true
private; it's convention (_protected,__name_mangled). - Inheritance — reuse via subclassing;
super()and C3-linearized MRO drive method resolution. - Polymorphism — Python leans on duck typing: any object with the right methods works, no shared base class required.
- Abstraction — hide details behind a clean interface, often via
abc.ABCor@property.
A common follow-up: when should you NOT use OOP? For simple data containers reach for @dataclass or NamedTuple; for stateless logic, plain functions and modules are often cleaner than forcing a class.
Follow-up 1
What are the main principles of object-oriented programming?
The main principles of object-oriented programming are:
Encapsulation: This principle involves bundling data and methods together within a class, hiding the internal details of how the class works from the outside world.
Inheritance: This principle allows a class to inherit attributes and methods from another class, promoting code reuse and creating a hierarchy of classes.
Polymorphism: This principle allows objects of different classes to be treated as objects of a common superclass, enabling code to be written that can work with objects of multiple types.
Abstraction: This principle involves creating abstract classes or interfaces that define a common set of methods that subclasses must implement. This allows for code to be written that can work with objects at a higher level of abstraction.
Follow-up 2
Can you give an example of how to define a class in Python?
Sure! Here's an example of how to define a simple class in Python:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
# Creating an instance of the Person class
person = Person("John", 25)
# Calling the greet method
person.greet()
Output:
Hello, my name is John and I am 25 years old.
Follow-up 3
How is inheritance implemented in Python?
Inheritance in Python is implemented using the syntax class ChildClass(ParentClass):. The child class (also known as the derived class) inherits attributes and methods from the parent class (also known as the base class). The child class can then add its own attributes and methods, and override or extend the ones inherited from the parent class.
Here's an example:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print("The animal makes a sound.")
# Dog class inherits from Animal class
class Dog(Animal):
def speak(self):
print("The dog barks.")
# Creating instances of the classes
animal = Animal("Animal")
dog = Dog("Dog")
# Calling the speak method
animal.speak() # Output: The animal makes a sound.
dog.speak() # Output: The dog barks.
Follow-up 4
What is the difference between a class and an object in Python?
In Python, a class is a blueprint or a template for creating objects, while an object is an instance of a class. A class defines the attributes and methods that an object will have, but it is not an actual object itself. When you create an object from a class, you are creating a specific instance of that class with its own unique set of attribute values.
For example:
class Person:
def __init__(self, name):
self.name = name
# Creating objects from the Person class
person1 = Person("John")
person2 = Person("Jane")
Follow-up 5
Can you explain the concept of encapsulation with an example in Python?
Encapsulation is the principle of bundling data and methods together within a class and hiding the internal details of how the class works from the outside world. It allows for data abstraction and provides control over the access to the data.
Here's an example in Python:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.__balance = balance # Private attribute
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient balance.")
def get_balance(self):
return self.__balance
# Creating an instance of the BankAccount class
account = BankAccount("123456789", 1000)
# Accessing the public methods
account.deposit(500)
account.withdraw(200)
# Accessing the private attribute (not recommended)
print(account._BankAccount__balance)
2. What is the purpose of the __init__ method in Python classes?
__init__ is the initializer — it runs automatically right after an object is created, to set up its instance attributes. The first parameter, self, is the new instance; you assign its state inside.
class Account:
def __init__(self, owner: str, balance: float = 0.0) -> None:
self.owner = owner
self.balance = balance
Key gotchas interviewers probe:
- It's not the constructor.
__new__actually creates the object;__init__only initializes the already-created instance.__init__must returnNone. - Mutable default arguments are a trap.
def __init__(self, items=[])shares one list across all instances. UseNoneand assign inside:
def __init__(self, items: list[int] | None = None) -> None:
self.items = items if items is not None else []
@dataclassgenerates__init__for you, reducing boilerplate for data-holding classes.- Calling
super().__init__(...)in a subclass is needed to run the parent's setup, especially under multiple inheritance.
Follow-up 1
Can you give an example of how to use the __init__ method?
Sure! Here's an example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person('John', 25)
print(person.name) # Output: John
print(person.age) # Output: 25
In this example, the Person class has an __init__ method that takes two parameters, name and age. When an object of the Person class is created, the __init__ method is automatically called with the provided arguments, and the name and age attributes of the object are initialized.
Follow-up 2
What happens if a class does not have an __init__ method?
If a class does not have an __init__ method, Python will automatically create a default __init__ method for the class. This default method does not do anything and does not take any parameters. However, if you want to initialize attributes or perform any setup when creating objects from the class, it is recommended to define your own __init__ method.
Follow-up 3
Can the __init__ method return a value?
No, the __init__ method cannot return a value. Its purpose is to initialize the object, not to return a value. If you need to return a value from a class method, you can define another method for that purpose.
Follow-up 4
Can we call the __init__ method explicitly on an object?
Yes, you can call the __init__ method explicitly on an object, but it is not recommended. The __init__ method is automatically called when an object is created, so there is usually no need to call it explicitly. However, if you have a specific use case where you want to re-initialize the object's attributes, you can call the __init__ method explicitly. Here's an example:
class Person:
def __init__(self, name):
self.name = name
def change_name(self, new_name):
self.__init__(new_name)
person = Person('John')
print(person.name) # Output: John
person.change_name('Alice')
print(person.name) # Output: Alice
In this example, the Person class has a change_name method that calls the __init__ method to re-initialize the name attribute with a new value.
3. Can you explain the concept of inheritance in Python?
Inheritance lets a class (the child/derived) reuse and extend the attributes and methods of another (the parent/base). It models an "is-a" relationship and reduces duplication.
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str: # override
return f"{self.name} says woof"
The child can override methods and still reach the parent's version via super():
class Puppy(Dog):
def speak(self) -> str:
return super().speak() + " (tiny)"
What interviewers really dig into:
super()and the MRO. Python resolves methods using C3 linearization — inspect it withDog.__mro__. With multiple inheritance,super()follows the MRO, not just the literal parent.- Prefer composition over inheritance when the relationship is "has-a," not "is-a." Deep hierarchies are fragile.
isinstance()/issubclass()check relationships at runtime; abstract base classes (abc.ABC) enforce that subclasses implement required methods.
Follow-up 1
What is the difference between single and multiple inheritance?
In single inheritance, a class inherits from only one parent class. This means that the child class can access and use the attributes and methods of the parent class.
In multiple inheritance, a class can inherit from multiple parent classes. This means that the child class can access and use the attributes and methods of all the parent classes. However, multiple inheritance can lead to method name conflicts if two or more parent classes have methods with the same name. In such cases, the method resolution order (MRO) determines which method is called.
Follow-up 2
Can you give an example of multiple inheritance in Python?
Sure! Here's an example of multiple inheritance in Python:
class ParentClass1:
def method1(self):
print('This is method 1 from ParentClass1')
class ParentClass2:
def method2(self):
print('This is method 2 from ParentClass2')
class ChildClass(ParentClass1, ParentClass2):
pass
child = ChildClass()
child.method1() # Output: This is method 1 from ParentClass1
child.method2() # Output: This is method 2 from ParentClass2
In this example, the ChildClass inherits from both ParentClass1 and ParentClass2. As a result, the ChildClass can access and use the methods method1 from ParentClass1 and method2 from ParentClass2.
Follow-up 3
What is the use of super() function in Python?
The super() function is used to call a method from the parent class. It is commonly used in the child class to invoke the parent class's implementation of a method while adding additional functionality.
Here's an example that demonstrates the use of super():
class ParentClass:
def method(self):
print('This is the parent class method')
class ChildClass(ParentClass):
def method(self):
super().method()
print('This is the child class method')
child = ChildClass()
child.method()
Output:
This is the parent class method
This is the child class method
In this example, the ChildClass overrides the method of the ParentClass. However, it still calls the method of the ParentClass using super().method() before adding its own functionality.
Follow-up 4
How does Python resolve the method resolution order in case of multiple inheritance?
Python uses a method resolution order (MRO) algorithm called C3 linearization to determine the order in which methods are resolved in case of multiple inheritance. The MRO ensures that each method in the inheritance hierarchy is called exactly once and in the correct order.
The MRO algorithm follows three main rules:
- Depth-first search: The algorithm starts with the leftmost parent class and traverses the inheritance hierarchy depth-first.
- Left-to-right ordering: When multiple parent classes are at the same level in the inheritance hierarchy, the algorithm follows a left-to-right ordering.
- First come, first served: If a method is found in multiple parent classes, the algorithm selects the method from the leftmost parent class first.
By following these rules, Python resolves the method order in a consistent and predictable manner.
4. What is polymorphism in Python?
Polymorphism means the same operation behaves differently depending on the object it acts on — "many forms." In Python it's driven mainly by duck typing: if an object has the right method, it works, regardless of its class.
class Dog:
def speak(self) -> str: return "woof"
class Cat:
def speak(self) -> str: return "meow"
def announce(animal) -> None: # no shared base class needed
print(animal.speak())
Forms interviewers expect you to distinguish:
- Duck typing — the Pythonic default; "if it walks like a duck..." No common superclass required.
- Method overriding — a subclass redefines a parent method (runtime polymorphism).
- Operator/dunder polymorphism —
+works on ints, strings, and lists because each type implements__add__. Implement dunders to make your own types polymorphic. functools.singledispatch— gives you type-based function overloading, which Python lacks natively (it has no method overloading by signature — the last definition wins).
A typical follow-up: how is this different from overloading? Python has no compile-time overloading; you use default args, *args, or singledispatch instead.
Follow-up 1
Can you give an example of polymorphism in Python?
Sure! Here's an example of polymorphism in Python:
class Animal:
def sound(self):
pass
class Dog(Animal):
def sound(self):
return 'Woof!'
class Cat(Animal):
def sound(self):
return 'Meow!'
def make_sound(animal):
print(animal.sound())
dog = Dog()
cat = Cat()
make_sound(dog) # Output: 'Woof!'
make_sound(cat) # Output: 'Meow!'
Follow-up 2
What is the difference between static and dynamic polymorphism?
Static polymorphism, also known as compile-time polymorphism, is achieved through function overloading and operator overloading. It is resolved at compile-time based on the number and types of arguments passed to a function or operator.
Dynamic polymorphism, also known as runtime polymorphism, is achieved through method overriding. It is resolved at runtime based on the actual type of the object being referred to.
Follow-up 3
How does Python handle operator overloading?
In Python, operator overloading is achieved by defining special methods or magic methods that are invoked when certain operators are used on objects. These methods have special names that start and end with double underscores, such as __add__ for the + operator.
For example, to overload the + operator for a custom class, you can define the __add__ method in the class. When the + operator is used on objects of that class, the __add__ method is called.
Here's an example:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2 # Calls the __add__ method
print(v3.x, v3.y) # Output: 4 6
Follow-up 4
Can you explain the concept of method overriding in Python?
Method overriding is a feature of object-oriented programming that allows a subclass to provide a different implementation of a method that is already defined in its superclass. In Python, method overriding is achieved by defining a method with the same name in the subclass.
When a method is called on an object of the subclass, Python first looks for the method in the subclass. If the method is found, the implementation in the subclass is executed. If the method is not found in the subclass, Python looks for it in the superclass.
Here's an example:
class Animal:
def sound(self):
return 'Animal sound'
class Dog(Animal):
def sound(self):
return 'Woof!'
animal = Animal()
dog = Dog()
print(animal.sound()) # Output: 'Animal sound'
print(dog.sound()) # Output: 'Woof!'
5. What is encapsulation in Python?
Encapsulation bundles data with the methods that operate on it inside a class, and controls how that data is accessed from outside. The goal is a clean public interface that hides internal details.
The catch — and the point interviewers want you to make — is that Python has no true access modifiers. Encapsulation is by convention:
_single_underscore— "internal, please don't touch." Purely a convention; nothing enforces it.__double_underscore— triggers name mangling (__xbecomes_ClassName__x), which avoids accidental clashes in subclasses but is not real privacy.
class Account:
def __init__(self, balance: float) -> None:
self._balance = balance # "protected" by convention
@property
def balance(self) -> float: # controlled read access
return self._balance
@balance.setter
def balance(self, value: float) -> None:
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
Follow-ups: use @property to expose validated, computed, or read-only attributes without changing the public API. acct._Account__x still reaches a mangled name — proof there's nothing truly private; encapsulation in Python is a discipline, not a guarantee.
Follow-up 1
How can we access private attributes in Python?
Although private attributes are intended to be accessed only within the class, Python provides a way to access them using name mangling. Name mangling is done by adding _ClassName as a prefix to the attribute name. Here's an example:
class MyClass:
def __init__(self):
self.__private_attr = 'Private attribute'
obj = MyClass()
print(obj._MyClass__private_attr) # Output: Private attribute
Follow-up 2
Can you give an example of encapsulation in Python?
Sure! Here's an example:
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
self.__mileage = 0
def drive(self, distance):
self.__mileage += distance
def get_mileage(self):
return self.__mileage
my_car = Car('Toyota', 'Camry')
my_car.drive(100)
print(my_car.get_mileage()) # Output: 100
Follow-up 3
What is the use of private attributes in Python?
Private attributes in Python are used to encapsulate data and prevent direct access from outside the class. They are denoted by prefixing the attribute name with double underscores (e.g., __attribute). By making attributes private, we can control how they are accessed and modified, and ensure data integrity and security.
Follow-up 4
What is the difference between public, private and protected attributes in Python?
In Python, attributes can be classified into three types based on their visibility and accessibility:
Public attributes: These attributes can be accessed and modified from anywhere, both inside and outside the class.
Private attributes: These attributes are intended to be accessed only within the class. They are denoted by prefixing the attribute name with double underscores (e.g.,
__attribute). Accessing them directly from outside the class raises anAttributeError.Protected attributes: These attributes are intended to be accessed only within the class and its subclasses. They are denoted by prefixing the attribute name with a single underscore (e.g.,
_attribute). Although they can be accessed from outside the class, it is considered a convention that they should not be accessed directly.
6. What are dataclasses, and when would you use them over a normal class or NamedTuple?
A modern-Python staple. @dataclass (3.7+) auto-generates boilerplate — __init__, __repr__, __eq__ — from annotated fields, so you write intent, not ceremony:
from dataclasses import dataclass, field
@dataclass(slots=True)
class Point:
x: int
y: int = 0
tags: list[str] = field(default_factory=list) # avoids the mutable-default trap
Why/when:
- Use it for data-holding classes where you'd otherwise hand-write
__init__/__repr__/__eq__. frozen=Truemakes instances immutable and hashable;slots=True(3.10+) cuts memory and speeds attribute access;order=Trueadds comparison methods.field(default_factory=...)is the correct way to give list/dict defaults (sidesteps the shared-mutable-default bug).
Contrast for the follow-up: NamedTuple/namedtuple is immutable, tuple-like, and indexable but lighter; TypedDict types plain dicts; pydantic.BaseModel adds runtime validation/parsing (used by FastAPI). Choose dataclass for plain in-memory records, pydantic when you need to validate external/untrusted data.
Live mock interview
Mock interview: Python Object-Oriented Programming
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.