Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›Python Interview Questions — OOP

Interview Prep

Python Interview Questions — OOP

Object-Oriented Programming (OOP) in Python is a paradigm based on the concept of 'objects' that bundle data and functionality together. It matters because it allows developers to model complex real-world entities into manageable, reusable structures. You should reach for OOP when your system requires maintaining state across multiple functions or when you need to enforce consistent data structures through inheritance and polymorphism.

The Core Concept: Classes vs. Instances

At the heart of Python's OOP is the distinction between a class and an instance. A class acts as a blueprint or a template, defining the structure and behavior of objects that will be created later. When you instantiate a class, you create a concrete object in memory that contains its own specific data. The 'self' keyword is critical here: it is an explicit reference to the instance currently being operated upon. Without 'self', the method would not know which object's specific attribute to modify. By defining data and methods within a class, you achieve encapsulation, ensuring that the logic governing that data is physically located near the data itself. This makes the code easier to maintain, as changes to the class definition immediately propagate to all future instances created from that blueprint, preventing fragmented logic across your application.

class UserProfile:
    def __init__(self, username, email):
        # 'self' allows us to attach specific data to this instance
        self.username = username
        self.email = email

    def display(self):
        return f"User: {self.username}, Email: {self.email}"

# Create two distinct objects from the same template
admin = UserProfile("admin", "admin@system.com")
guest = UserProfile("guest", "guest@system.com")
print(admin.display())

Managing Lifecycle: Constructors and Destructors

Every object in Python follows a lifecycle managed by special methods, often called 'dunder' (double underscore) methods. The '__init__' method is the constructor, which is called automatically upon object creation to set up initial state. Conversely, the '__del__' method acts as a destructor, though in Python, it is rarely needed due to the garbage collector. Understanding these methods is essential because it allows you to handle resource allocation. For example, if your object opens a network connection or a file, you want that process to start exactly when the object is initialized. By centralizing initialization logic in the constructor, you ensure that no object is ever created in an invalid or incomplete state. This reduces runtime errors significantly because the class guarantees that its internal attributes are properly configured before any other methods are invoked by the consumer.

class DatabaseConnection:
    def __init__(self, db_name):
        # Initialize resources when object is created
        self.db_name = db_name
        print(f"Opening connection to {self.db_name}...")

    def __del__(self):
        # Cleanup when the object is garbage collected
        print(f"Closing connection to {self.db_name}.")

# The object lifecycle is managed automatically
db = DatabaseConnection("production_db")
del db  # Manual trigger for demonstration

Inheritance and Code Reusability

Inheritance allows a class (the child) to derive attributes and methods from another class (the parent). This is not merely for saving typing; it is for creating an 'is-a' relationship that enforces structural consistency. When you design a hierarchy, you define the general behavior in the parent class and specialize the behavior in the child classes. The 'super()' function is the standard way to call methods from the parent class within the child, ensuring that initialization logic defined at higher levels is not skipped. By using inheritance, you can design systems where you can treat different objects as the same base type, allowing for more flexible code. This paradigm is particularly useful in large systems where you need to implement various types of a specific tool while keeping the core administrative logic consistent across all variations, preventing code duplication across the entire project structure.

class Appliance:
    def __init__(self, brand):
        self.brand = brand

class Toaster(Appliance):
    def __init__(self, brand, slots):
        # Call parent constructor to handle shared logic
        super().__init__(brand)
        self.slots = slots

t = Toaster("KitchenAid", 2)
print(f"Brand: {t.brand}, Slots: {t.slots}")

Polymorphism: Consistent Interfaces

Polymorphism enables different object types to respond to the same method call in their own specific ways. This is the cornerstone of writing extensible code because the consuming function does not need to know the specific class of an object; it only needs to know that the object implements a specific interface. For instance, a function might call 'process()' on a list of objects. Whether that object is a 'Document', 'Image', or 'Video', as long as they all share a 'process()' method, the code remains clean and functional. This eliminates the need for complex type-checking or chains of 'if-else' statements that grow exponentially as you add new types. By relying on common interfaces rather than specific class types, your code becomes modular, allowing you to plug in new types into existing logic without ever needing to modify the underlying caller code.

class PDFReport:
    def render(self): return "Rendering PDF"

class HTMLReport:
    def render(self): return "Rendering HTML"

# The function doesn't care about the object type
# as long as it has a 'render' method.
def generate(report):
    print(report.render())

generate(PDFReport())
generate(HTMLReport())

Encapsulation and Data Protection

Encapsulation is the practice of restricting direct access to an object's internal data. In Python, while there is no strict hardware-level enforcement, we use a single underscore prefix to signify 'protected' attributes and a double underscore prefix for 'private' attributes. This is important because it protects the integrity of the object state. By forcing users to interact with your object through 'getter' and 'setter' methods (often implemented via the '@property' decorator), you can introduce validation logic. For example, if a user tries to set a negative value for an 'age' attribute, your setter can raise an error instead of allowing the state to become invalid. This abstraction layer ensures that you can change the internal implementation of your class without breaking the code that relies on your public methods, effectively shielding your internal logic from external misuse.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance # Private attribute

    @property
    def balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

acc = BankAccount(100)
acc.deposit(50)
print(acc.balance)

Key points

  • Classes serve as templates for creating objects, while instances are the specific implementations of those templates.
  • The 'self' keyword provides a necessary reference to the current instance so methods can access object-specific data.
  • Special dunder methods like '__init__' are used to hook into the lifecycle of objects for initialization and cleanup tasks.
  • Inheritance establishes an 'is-a' relationship, allowing child classes to inherit and extend functionality from parent classes.
  • The 'super()' function is essential for delegating initialization tasks to parent classes within an inheritance chain.
  • Polymorphism allows different objects to share the same method names, which promotes modular and extensible design.
  • Encapsulation protects object state by hiding internal data and forcing interaction through controlled interfaces like properties.
  • Using '@property' decorators allows for data validation during access, ensuring that objects remain in a valid state throughout their lifetime.

Common mistakes

  • Mistake: Modifying class attributes directly via an instance. Why it's wrong: This creates an instance variable that shadows the class variable, leading to unexpected behavior. Fix: Always modify class attributes using the class name itself.
  • Mistake: Misunderstanding the 'self' parameter. Why it's wrong: Users often try to pass 'self' manually or omit it from method definitions, causing TypeError. Fix: 'self' is automatically passed by the interpreter; always include it as the first argument in instance methods.
  • Mistake: Overusing inheritance instead of composition. Why it's wrong: Deep inheritance hierarchies create tight coupling and brittle code. Fix: Use composition ('has-a' relationship) to build complex objects from simpler, modular components.
  • Mistake: Not calling the base class constructor. Why it's wrong: Failing to call super().__init__() prevents the base class from initializing its state, causing partial object instantiation. Fix: Always explicitly call super().__init__() in the subclass constructor.
  • Mistake: Treating underscore prefixes as true private access modifiers. Why it's wrong: Python does not have strict private members; leading underscores are merely a convention. Fix: Use double underscores for name mangling if you need to avoid name collisions, but acknowledge it is not a security feature.

Interview questions

What is the fundamental purpose of a class in Python, and how does it differ from an object?

A class in Python acts as a blueprint or a template for creating objects. It defines the attributes (data) and methods (behaviors) that all objects of that type will possess. An object, conversely, is an instance of that class; it is the concrete realization that holds specific state data. For example, if 'Car' is the class, a specific red sedan is the object. The class defines the structure, while the object utilizes that structure to store unique information, allowing for organized and modular code architecture.

Explain the role of the __init__ method and the importance of the 'self' parameter in Python classes.

The __init__ method is a special constructor method that is automatically invoked when a new instance of a class is created. Its primary role is to initialize the attributes of the object. The 'self' parameter represents the specific instance of the object being created or manipulated. By using 'self', Python ensures that methods can access and modify the attributes belonging to that particular instance. Without 'self', the method would not know which object's state it is intended to update, leading to scope conflicts within the class structure.

How does inheritance work in Python, and why would you use it when designing software?

Inheritance allows a child class to derive attributes and methods from a parent class, facilitating code reuse and logical hierarchies. You use it when you have a 'is-a' relationship, such as a 'Dog' being a type of 'Animal'. By inheriting from the parent, the child class automatically gains the parent's functionality, which can then be extended or overridden. This reduces redundancy because you do not have to rewrite existing logic, making maintenance much easier as updates to the base class propagate to all subclasses automatically.

Compare composition versus inheritance in Python: when should you favor one over the other?

Inheritance is best used for 'is-a' relationships where you want to share common behavior across related types. However, composition—where a class contains instances of other classes—is often preferred for 'has-a' relationships. Composition is generally more flexible because it avoids the 'fragile base class' problem, where changes to a parent class unintentionally break deeply nested child classes. Favor composition when you want to combine discrete features rather than creating a rigid taxonomic hierarchy, as it keeps your objects decoupled and easier to test in isolation.

What are public, protected, and private attributes in Python, and how does Python handle encapsulation?

In Python, encapsulation is primarily based on convention rather than strict enforcement. Public attributes are accessible from anywhere. Protected attributes are prefixed with a single underscore (e.g., _value), signaling they should not be accessed directly outside the class. Private attributes use a double underscore (e.g., __value), which triggers name mangling, making them harder to access from outside. This approach protects internal state from unintended modification while allowing developers to define clear interfaces for how their objects should be interacted with by external code.

What is method overriding, and how do you use 'super()' effectively in this context?

Method overriding occurs when a subclass defines a method that already exists in its parent class, allowing the child to provide a specific implementation. You use 'super()' inside the child class to invoke the original method from the parent class before or after adding custom logic. This is crucial because it allows the subclass to leverage the base functionality while extending it. For example, in an overridden __init__, calling 'super().__init__()' ensures that the parent’s setup logic is executed, preventing incomplete object initialization.

All Python interview questions →

Check yourself

1. What happens when you define a method with a double underscore prefix (e.g., __method) in a Python class?

  • A.The method becomes completely inaccessible from outside the class.
  • B.The interpreter performs name mangling, renaming the attribute to _ClassName__method.
  • C.The method is automatically marked as static by the compiler.
  • D.The method is strictly private and will raise an AttributeError if called externally.
Show answer

B. The interpreter performs name mangling, renaming the attribute to _ClassName__method.
Option 1 is wrong because it can still be accessed via mangled names. Option 2 is correct as it describes how Python avoids collisions in subclasses. Option 3 is wrong because it has no effect on static status. Option 4 is wrong because Python does not enforce strict privacy.

2. In Python's Method Resolution Order (MRO), how does the language determine which method to call in a multiple inheritance scenario?

  • A.It uses a depth-first search, visiting the child, then the first parent, then the grandparent.
  • B.It uses the C3 Linearization algorithm to provide a consistent and predictable method ordering.
  • C.It prioritizes the method defined in the most recently inherited base class.
  • D.It defaults to the method found in the 'object' class if multiple parents define it.
Show answer

B. It uses the C3 Linearization algorithm to provide a consistent and predictable method ordering.
Option 1 describes the old-style class behavior. Option 2 is correct as C3 Linearization is the standard used for reliable MRO. Option 3 is wrong because MRO is fixed at class definition. Option 4 is wrong as it doesn't describe the order of inheritance.

3. When defining a class method, what is the primary purpose of the '@classmethod' decorator?

  • A.It allows the method to access instance variables without creating an object.
  • B.It changes the first argument from 'self' to 'cls', allowing access to class state.
  • C.It prevents the method from being overridden by subclasses.
  • D.It increases the execution speed of the method during object instantiation.
Show answer

B. It changes the first argument from 'self' to 'cls', allowing access to class state.
Option 1 is wrong because instance variables require an instance. Option 2 is correct because it passes the class itself. Option 3 is wrong as decorators don't restrict overriding. Option 4 is wrong as it is a architectural choice, not a performance optimization.

4. What is the primary difference between '__init__' and '__new__' in Python?

  • A.__init__ creates the object, while __new__ initializes the object's attributes.
  • B.__new__ is used for static methods, while __init__ is for instance methods.
  • C.__new__ is the constructor responsible for creating the instance, while __init__ initializes it.
  • D.__init__ can return an object, while __new__ must return None.
Show answer

C. __new__ is the constructor responsible for creating the instance, while __init__ initializes it.
Option 1 is backwards. Option 2 is incorrect. Option 3 is correct: __new__ creates the object, and __init__ sets the initial state. Option 4 is wrong because __new__ MUST return an instance, whereas __init__ returns None.

5. Why is it considered better practice to use properties (@property) rather than public attributes in Python?

  • A.Properties are automatically thread-safe, unlike public attributes.
  • B.Properties allow for the implementation of getters and setters without changing the external API.
  • C.Properties are required for any class to be compatible with built-in functions.
  • D.Properties are the only way to make an attribute read-only.
Show answer

B. Properties allow for the implementation of getters and setters without changing the external API.
Option 1 is wrong; properties don't provide thread safety. Option 2 is correct; they allow adding logic to attribute access while keeping usage as simple as 'obj.x'. Option 3 is wrong. Option 4 is wrong because there are other ways to restrict access, though properties are the standard way.

Take the full Python quiz →

← PreviousPython Interview Questions — Data StructuresNext →Python Interview Questions — Advanced

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app