Skip to content
Home Interview Python OOP Interview Questions: Concepts, Code & Gotchas

Python OOP Interview Questions: Concepts, Code & Gotchas

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Interview → Topic 2 of 4
Python OOP interview questions explained with real code, analogies, and insider tips.
⚙️ Intermediate — basic Interview knowledge assumed
In this tutorial, you'll learn
Python OOP interview questions explained with real code, analogies, and insider tips.
  • The four pillars only matter in interviews when you tie each one to a concrete problem it solves — 'encapsulation hides the Stripe API key from the calling code' beats 'encapsulation bundles data and methods' every time.
  • Prefer composition over inheritance when you want to combine behaviours — inject a GPSModule into a SmartCar rather than making SmartCar inherit from GPSModule. Inheritance is for genuine 'is-a' relationships only.
  • @classmethod is Python's pattern for alternative constructors because it receives cls and can therefore build and return an instance — that's the only reason it exists over @staticmethod.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

Think of Object-Oriented Programming like building with LEGO. Each LEGO brick type is a 'class' — a blueprint that says what shape the brick is and what it can do. When you actually snap a brick into your model, that's an 'object' — a real thing built from the blueprint. Inheritance is like a special brick that's based on an existing one but has an extra knob. You don't redesign the whole brick; you just extend what's already there.

Python interviews at companies like Google, Stripe, and mid-sized startups almost always include OOP questions — not because the interviewers want you to recite definitions, but because how you talk about OOP reveals whether you actually design software or just write scripts. A candidate who can explain why encapsulation exists (not just what it is) stands out immediately from the crowd who memorised a textbook definition the night before.

OOP solves the problem of code that grows into an unmanageable tangle. Without it, adding a new feature means hunting through hundreds of lines of procedural code, hoping you don't break something else. With OOP, you model your problem as a collection of objects that each own their own data and behaviour — so changes stay contained, reuse becomes natural, and testing gets easier.

By the end of this article you'll be able to explain the four pillars of OOP with real examples, write and debug class hierarchies in Python, spot the classic mistakes candidates make under pressure, and answer the follow-up questions that actually trip people up in interviews.

The Four Pillars — What They Are and Why They Exist

Every OOP interview starts here. Interviewers don't just want the names — they want to see you connect each pillar to a real problem it solves.

Encapsulation bundles data and the methods that act on it into one unit, and hides the messy internals. Think of a TV remote: you press a button, the TV changes channel. You don't need to know about the infrared signal. The remote encapsulates that complexity.

Inheritance lets a new class reuse behaviour from an existing one. Instead of copy-pasting a send_email method into five classes, you put it in a base Notification class and let the others inherit it. Changes in one place propagate everywhere.

Polymorphism means different objects can respond to the same method call in their own way. You call .render() on a Button and on a Chart — Python figures out which version to run. Your calling code doesn't need an if-else chain.

Abstraction hides what the implementation does and exposes only what it accomplishes. An abstract PaymentProcessor class forces every subclass (Stripe, PayPal) to implement .charge(), but callers never care which one they're using.

io/thecodeforge/oop/pillars_demo.py · PYTHON
12345678910111213141516171819202122232425262728293031323334
from abc import ABC, abstractmethod

# ABSTRACTION: Defining the interface. 
# We don't care HOW a gateway works, only that it can authorize and capture.
class PaymentGateway(ABC):
    @abstractmethod
    def authorize(self, amount: float) -> bool:
        pass

    @abstractmethod
    def capture(self, transaction_id: str) -> bool:
        pass

# INHERITANCE: Specific implementation of the abstraction.
class StripeGateway(PaymentGateway):
    def __init__(self, api_secret: str):
        # ENCAPSULATION: Internal credential hidden from the public API
        self.__api_secret = api_secret

    def authorize(self, amount: float) -> bool:
        print(f"[Stripe] Authorizing ${amount}...")
        return True

    def capture(self, transaction_id: str) -> bool:
        print(f"[Stripe] Capturing {transaction_id}")
        return True

# POLYMORPHISM: The caller treats all gateways the same.
def process_order(gateway: PaymentGateway, amount: float):
    if gateway.authorize(amount):
        gateway.capture("tx_123")

gateway = StripeGateway(api_secret="sk_test_4eC39HqLyj")
process_order(gateway, 99.99)
▶ Output
[Stripe] Authorizing $99.99...
[Stripe] Capturing tx_123
💡Interview Gold:
When asked to define a pillar, always follow your definition with 'and that matters because…'. For example: 'Polymorphism means one interface, many implementations — and that matters because it lets you add a new payment provider without touching any of the existing calling code.' That one sentence separates you from every candidate who just recited a definition.

Inheritance vs Composition — The Question That Trips Everyone Up

Interviewers love asking 'when would you use inheritance over composition?' because most candidates either go blank or recite 'favour composition over inheritance' without knowing why.

Inheritance models an is-a relationship. A Dog is an Animal. That relationship is rigid — once you inherit from a class, you're locked into its interface and any side-effects of its implementation changes.

Composition models a has-a relationship. A Car has an Engine. You can swap the engine (electric vs petrol) without rewriting the car. This is why composition is usually more flexible.

The practical rule: use inheritance when the child genuinely is a specialised version of the parent and you want to enforce a shared contract. Use composition when you want to combine behaviours, because it keeps each piece small, testable, and swappable.

The classic trap is building deep inheritance chains (six levels deep) only to find that a change in the grandparent breaks three grandchildren in unpredictable ways. Composition avoids that cascade entirely.

io/thecodeforge/oop/composition_vs_inheritance.py · PYTHON
123456789101112131415161718192021222324252627
import uuid

# COMPOSITION: Behaviours as separate components
class Logger:
    def log(self, message: str):
        print(f"[LOG] {message}")

class Repository:
    def save(self, data: dict):
        print(f"[DB] Saving {data['id']}")

# The Application Service HAS-A logger and HAS-A repository
class UserService:
    def __init__(self, logger: Logger, repo: Repository):
        self.logger = logger
        self.repo = repo

    def create_user(self, username: str):
        user_id = str(uuid.uuid4())
        self.logger.log(f"Creating user {username}")
        self.repo.save({"id": user_id, "username": username})

# Usage
logger = Logger()
repo = Repository()
service = UserService(logger, repo)
service.create_user("dev_forge")
▶ Output
[LOG] Creating user dev_forge
[DB] Saving <uuid>
⚠ Watch Out:
Never inherit just to reuse a method. If the child class isn't genuinely a type of the parent, you'll end up with nonsensical relationships like a PDF inheriting from Printer just to get a format() method. That's composition's job — inject a Formatter object instead.

Dunder Methods, Properties, and Class vs Static Methods

These three topics show up constantly in Python OOP interviews because they reveal whether you understand Python's OOP model specifically — not just OOP in general.

Dunder (magic) methods let your objects integrate with Python's built-in syntax. Define __str__ so print(my_object) shows something meaningful. Define __eq__ so order_a == order_b compares the right fields. Define __len__ so len(my_cart) works naturally. They're called 'dunder' because they have Double UNDERscores on each side.

Properties (via @property) let you expose a clean attribute interface while hiding validation logic behind it. You read employee.salary like an attribute, but under the hood Python calls a getter method. The caller never sees the implementation change if you add validation later.

Class methods vs static methods trip up almost everyone. A @classmethod receives the class itself as its first argument (cls) — it can create instances, so it's perfect for alternative constructors. A @staticmethod receives nothing special — it's just a utility function that logically belongs inside the class namespace but doesn't need access to the class or instance.

io/thecodeforge/oop/advanced_methods.py · PYTHON
12345678910111213141516171819202122232425262728293031
class DatabaseConnection:
    _instances = 0

    def __init__(self, connection_string: str):
        self._connection_string = connection_string
        self.__class__._instances += 1

    # Dunder for debugging
    def __repr__(self):
        return f"DatabaseConnection('{self._connection_string}')"

    # Property for controlled access
    @property
    def status(self) -> str:
        return "Connected" if self._connection_string else "Disconnected"

    # Class Method: Alternative Constructor
    @classmethod
    def from_env(cls):
        # In real world: fetch from os.getenv
        return cls("postgres://localhost:5432/forge")

    # Static Method: Pure utility
    @staticmethod
    def is_valid_uri(uri: str) -> bool:
        return uri.startswith("postgres://")

conn = DatabaseConnection.from_env()
print(f"Status: {conn.status}")
print(f"Representation: {repr(conn)}")
print(f"Is Valid? {DatabaseConnection.is_valid_uri('http://bad.uri')}")
▶ Output
Status: Connected
Representation: DatabaseConnection('postgres://localhost:5432/forge')
Is Valid? False
🔥Interview Gold:
When asked about @classmethod vs @staticmethod, say: 'A classmethod is an alternative constructor — it can build and return instances because it has a reference to the class itself via cls. A staticmethod is just a namespaced function — use it when the logic belongs conceptually to the class but doesn't need to touch class or instance state.' Then mention Employee.from_full_name() as your example. Concrete examples win interviews.

MRO, super(), and Multiple Inheritance — Python's Hidden Complexity

Multiple inheritance is rare in practice but almost always shows up in Python OOP interviews because it exposes whether you understand Python's Method Resolution Order (MRO).

When a class inherits from two parents that both define the same method, Python needs a rule for which one wins. That rule is the C3 linearisation algorithm, and the result is visible via ClassName.__mro__. Python reads it left to right — the first class in the MRO that defines the method wins.

super() doesn't just mean 'call the parent class'. In a multiple-inheritance chain, super() calls the next class in the MRO — which might not be the direct parent. This is the cooperative inheritance pattern, and it's why every class in a diamond hierarchy should call super().__init__() if it wants all initialisers to run correctly.

In practice, you'll see multiple inheritance most often with mixins — small classes that add a single, specific behaviour (like LoggingMixin or SerializableMixin) without being a full standalone class. It's composition-flavoured inheritance, and it's elegant when kept small.

io/thecodeforge/oop/mro_super.py · PYTHON
12345678910111213141516171819202122
class Base:
    def __init__(self):
        print("Base init")

class MixinA(Base):
    def __init__(self):
        print("MixinA init")
        super().__init__()

class MixinB(Base):
    def __init__(self):
        print("MixinB init")
        super().__init__()

class Diamond(MixinA, MixinB):
    def __init__(self):
        print("Diamond init")
        super().__init__()

# Triggering the diamond
d = Diamond()
print(f"MRO: {[c.__name__ for c in Diamond.mro()]}")
▶ Output
Diamond init
MixinA init
MixinB init
Base init
MRO: ['Diamond', 'MixinA', 'MixinB', 'Base', 'object']
⚠ Watch Out:
If any class in a cooperative inheritance chain forgets to call super().__init__(), the MRO chain breaks silently — classes further down the chain never run their initialisers. You'll get AttributeErrors that look completely unrelated to the missing super() call. Always call super().__init__(args, *kwargs) in every class that's meant to participate in multiple inheritance.
Feature@classmethod@staticmethodInstance Method
First argumentcls (the class)None (no implicit arg)self (the instance)
Can access instance stateNoNoYes
Can access class stateYesNoYes (via self.__class__)
Can create instancesYes — perfect for thisPossible but awkwardYes
Callable on class directlyYesYesTechnically, but unusual
Primary use caseAlternative constructorsUtility / helper functionsCore object behaviour
Real-world exampleEmployee.from_csv(row)Employee.is_valid_salary(n)employee.get_pay_slip()

🎯 Key Takeaways

  • The four pillars only matter in interviews when you tie each one to a concrete problem it solves — 'encapsulation hides the Stripe API key from the calling code' beats 'encapsulation bundles data and methods' every time.
  • Prefer composition over inheritance when you want to combine behaviours — inject a GPSModule into a SmartCar rather than making SmartCar inherit from GPSModule. Inheritance is for genuine 'is-a' relationships only.
  • @classmethod is Python's pattern for alternative constructors because it receives cls and can therefore build and return an instance — that's the only reason it exists over @staticmethod.
  • In any multiple-inheritance chain, always call super().__init__(args, *kwargs) — forgetting it silently breaks the MRO chain and causes AttributeErrors that look unrelated to the real problem.

⚠ Common Mistakes to Avoid

    Mutable default argument in __init__ — writing `def __init__(self, items=[])` causes ALL instances to share the same list object. Add one item to instance A and it magically appears in instance B. Fix: use `def __init__(self, items=None)` and set `self.items = items if items is not None else []` inside the body.
    Fix

    use def __init__(self, items=None) and set self.items = items if items is not None else [] inside the body.

    Forgetting to call super().__init__() in a subclass — when your subclass overrides __init__ without calling super(), the parent's initialisation code never runs. You'll get AttributeErrors on attributes that the parent was supposed to set. Fix: always add `super().__init__(*args, **kwargs)` as the first line of your subclass __init__ unless you have a deliberate reason not to.
    Fix

    always add super().__init__(args, *kwargs) as the first line of your subclass __init__ unless you have a deliberate reason not to.

    Confusing __str__ and __repr__ — candidates define __str__ for debugging output, which means `repr(obj)` in a REPL or a log file shows something useless like `<__main__.Order object at 0x7f3a...>`. The rule: __repr__ must be unambiguous and ideally show enough info to recreate the object; __str__ is for end-user display. Always define both.

    efine both.

Interview Questions on This Topic

  • QWhat is the difference between a classmethod and a staticmethod in Python, and can you give a real-world use case where you'd choose each one?
  • QExplain Python's Method Resolution Order. If class C inherits from both A and B, and both A and B define a method called process(), which one does Python call and why?
  • QHow does Python enforce encapsulation? What's the difference between a single-underscore prefix, a double-underscore prefix, and using @property — and when would you reach for each?
  • QImplement a Singleton pattern in Python using a metaclass or the __new__ method. Explain the trade-offs.
  • QHow do Abstract Base Classes (ABCs) differ from Interfaces in Java/C#, and how does Python's 'Duck Typing' interact with them?

Frequently Asked Questions

What Python OOP concepts are most commonly tested in interviews?

The four pillars (encapsulation, inheritance, polymorphism, abstraction) are always tested, but interviewers at technical companies dig deeper into Python-specific features: dunder methods, @property, @classmethod vs @staticmethod, MRO, and the difference between abstract base classes and duck typing. Knowing the pillar names isn't enough — you need a code example for each.

What is the difference between __str__ and __repr__ in Python?

__repr__ is for developers — it should return an unambiguous string that ideally lets you recreate the object (e.g. Employee('Alice', 'Smith', 80000)). __str__ is for end users — it can be friendlier and shorter (e.g. Alice Smith @ TheCodeForge). When only one is defined, Python falls back to __repr__, so always define __repr__ first.

How do you implement a private attribute in Python?

Python uses name mangling for attributes with a double underscore (e.g., __secret). This transforms self.__secret into _ClassName__secret to prevent accidental access. However, it's not a strict access modifier. For standard encapsulation, use a single underscore _secret to signal internal use or the @property decorator to manage access logic.

Is Python truly object-oriented if it doesn't have true private attributes?

Python is object-oriented but takes a pragmatic approach: 'we're all adults here.' A single underscore (_name) is a strong convention meaning 'internal use only — don't touch this from outside.' A double underscore (__name) triggers name-mangling (it becomes _ClassName__name) which makes accidental external access harder but not impossible. Python relies on developer discipline rather than compiler enforcement — and in practice, this works well for the vast majority of codebases.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousTop 50 Python Interview QuestionsNext →Python Data Structures Interview Q
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged