Senior 7 min · March 06, 2026

Python OOP MRO Failure — Fix super().__init__ in Mixins

AttributeError on a parent attribute? In cooperative inheritance, a missing super().__init__ in a mixin breaks the MRO chain.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Python OOP interviews test if you design software or just write scripts
  • Four pillars: encapsulation, inheritance, polymorphism, abstraction — each solves a concrete problem
  • @classmethod is an alternative constructor; @staticmethod is a namespaced utility
  • super() in multiple inheritance calls the next class in MRO, not necessarily the parent
  • Biggest mistake: forgetting super().__init__() in a diamond chain — breaks initialization silently
✦ Definition~90s read
What is Python OOP Interview Questions?

Python's Method Resolution Order (MRO) is the algorithm that determines which method gets called when you invoke it on an object, especially under multiple inheritance. The classic failure mode occurs when you mix classes with super().__init__() calls in a diamond inheritance pattern — if one parent class doesn't call super().__init__(), the chain breaks silently, leaving some initializers unexecuted.

Think of Object-Oriented Programming like building with LEGO.

This isn't a Python bug; it's a design constraint that forces you to understand C3 linearization (the algorithm Python uses to compute MRO) and to adopt cooperative multiple inheritance patterns where every class in the hierarchy calls super().__init__() with matching signatures. Real-world frameworks like Django's class-based views and SQLAlchemy's declarative base rely on this working correctly, and getting it wrong produces bugs that are notoriously hard to trace.

This article covers the full spectrum of Python OOP concepts that trip up senior engineers in interviews: the four pillars (encapsulation, abstraction, inheritance, polymorphism) and why they exist as design tools rather than academic checkboxes; the inheritance-vs-composition decision that separates junior from senior thinking; dunder methods like __getattr__ and __setattr__ that control attribute access; properties as computed attributes with getter/setter logic; the difference between @classmethod, @staticmethod, and instance methods; and the modern Protocol-based approach to duck typing that replaces fragile hasattr checks. You'll also see how abstract base classes from abc enforce interfaces at runtime, while typing.Protocol provides structural subtyping without inheritance — a pattern used extensively in libraries like attrs and pydantic.

Where this matters most is in production codebases that mix concerns via multiple inheritance — think mixin classes for logging, caching, or serialization. When you understand MRO, you can predict exactly which __init__ runs and in what order, and you can design mixins that compose safely.

Without that understanding, you'll hit the 'MRO failure' where super().__init__() skips a parent or raises a TypeError about argument mismatches. This article gives you the mental model to debug those failures on sight and to answer the interview questions that separate engineers who've read the docs from those who've lived the bugs.

Plain-English First

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.

Why Python's MRO Fails in Mixins — And How super() Actually Works

Python's Method Resolution Order (MRO) determines which method gets called when you invoke super() in a class hierarchy. It's not just a linear parent search — it uses the C3 linearization algorithm to produce a consistent, monotonic order that respects all inheritance chains. For single inheritance, MRO is trivial; for multiple inheritance, it's the only thing preventing ambiguous or skipped method calls.

In practice, MRO is computed at class definition time and stored as __mro__. When you call super().__init__(), Python walks the MRO from the current class to the next class in the chain — not the parent class. This means super() in a mixin doesn't call the mixin's parent; it calls the next class in the MRO, which could be another mixin or the final base class. The order is determined by the child class's inheritance declaration, not the mixin's own bases.

You rely on MRO correctness every time you compose behaviors via mixins — logging, authentication, serialization. If you break the linearization (e.g., by introducing a diamond pattern with inconsistent base class ordering), Python raises TypeError: Cannot create a consistent method resolution order (MRO). This is not a warning — it's a hard failure at class definition time, preventing runtime ambiguity.

super() Is Not Parent()
super() in a mixin calls the next class in the MRO, not the mixin's direct parent. If you assume it calls a specific base, you'll skip __init__ chains silently.
Production Insight
A team added a CacheMixin before AuthMixin in a view class. CacheMixin.__init__ called super() expecting to reach AuthMixin, but MRO placed AuthMixin after the base HTTP handler — so cache init skipped auth entirely, leaking unauthenticated data into cache.
Symptom: intermittent 403 errors that vanished when mixin order was swapped, with no obvious code change.
Rule: always declare mixins left-to-right from most generic to most specific, and never assume super() targets a particular class — test the full MRO with ClassName.__mro__.
Key Takeaway
MRO is computed via C3 linearization at class definition time — you cannot change it at runtime.
super() in a mixin delegates to the next class in the MRO, not the parent class.
Break MRO consistency and Python raises TypeError at class definition — not at runtime.
Python MRO Failure in Mixins THECODEFORGE.IO Python MRO Failure in Mixins How super() resolves method calls in multiple inheritance MRO (Method Resolution Order) C3 linearization determines class hierarchy order super() in Mixins Delegates to next class in MRO, not parent super().__init__() Must be called in all cooperating classes Inconsistent super() Calls Breaks chain, skips initializations Cooperative Multiple Inheritance All classes use super() and same signature ⚠ Forgetting super() in one mixin breaks the entire chain Always call super().__init__() in every mixin class THECODEFORGE.IO
thecodeforge.io
Python MRO Failure in Mixins
Python Oop Interview Questions

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.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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.
Production Insight
Production codebases that ignore encapsulation leak internal state through getters and setters — refactoring becomes a minefield.
A deep inheritance chain (5+ levels) in a payment system required touching 7 classes to add a simple logging change — composition would have avoided it.
Rule: shallow inheritance, deep composition.
Key Takeaway
Tie each pillar to a concrete failure you've seen or anticipate.
Encapsulation prevents the Stripe secret from leaking into logs.
Polymorphism lets you swap PayPal for Stripe without changing calling code.
And that matters because it reduces blast radius.

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.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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.
Production Insight
A team inherited from a base 'Entity' class just to get a 'save()' method — then marketing asked for an 'Auditable' feature and the entire hierarchy broke.
The 3-level inheritance chain for 'Notification' caused a 3-day outage when the base class added a new abstract method.
Rule: if you can't state the relationship as 'X is a Y' without hesitation, use composition.
Key Takeaway
Inheritance is for is-a; composition is for has-a.
The moment you need to 'reuse' a method, reach for composition.
Deep inheritance chains ARE the tech debt that silently makes refactoring impossible.
Inheritance vs Composition Decision Tree
IfDoes the new class genuinely 'is a' subtype of the parent (e.g., Cat is an Animal)?
UseUse inheritance. You want to reuse and extend the parent's interface and behaviour.
IfDo you only need to reuse behaviour, not interface (e.g., Car needs Engine's start method)?
UseUse composition. Inject the Engine as a dependency. You can swap implementations later.
IfIs the relationship hierarchical and stable (e.g., Vehicle -> Car -> Sedan)?
UseInheritance works, but keep depth ≤ 3. Beyond that, use composition to avoid fragile base class.
IfAre you combining multiple behaviours (e.g., Logging + Serialization)?
UseComposition with separate Logging and Serialization components, or use mixins (Python) carefully.

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.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.
Production Insight
A logging system used @staticmethod for formatting, but later needed access to class-configurable log levels — had to refactor to @classmethod, breaking callers.
@Property setters without validation: a production bug where a negative price was stored in the database because the setter didn't validate.
Rule: prefer @classmethod over @staticmethod when the method might ever need class state.
Key Takeaway
Classmethod = alternative constructor (has cls).
Staticmethod = namespaced utility (no cls).
Property = attribute access with hidden computation.
Use @classmethod when you anticipate needing class context later.

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.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.
Production Insight
A Django model mixin hierarchy of 4 classes had one missing super() — migration scripts failed with no obvious cause.
Flask extension classes use MRO heavily; a custom extension broke when a new version added a dependency mixin.
Rule: in any diamond, every class must participate in cooperative super calls.
Key Takeaway
MRO is C3 linearisation: left-to-right, depth-first, then rightmost.
super() calls the next in MRO, not the literal parent.
Missing super().__init__() anywhere in the chain breaks all downstream initializers.

Abstract Base Classes, Duck Typing, and Protocols

Python's OOP interviews often probe how you handle interfaces without a formal interface keyword. The answer involves Abstract Base Classes (ABCs), duck typing, and the newer Protocol class from typing.

Abstract Base Classes force subclasses to implement certain methods. Use abc.ABC and @abstractmethod to define contracts. If a subclass doesn't implement all abstract methods, it cannot be instantiated — great for enforcing a shared API across a family of classes.

Duck Typing is Python's runtime philosophy: 'If it walks like a duck and quacks like a duck, it's a duck.' No explicit interface needed — just implement the expected methods. This is powerful but fragile: an object lacking a method only fails at runtime when that method is called.

Protocols (from typing.Protocol) are structural subtyping. You define a protocol class with method signatures, and any object that implements those methods satisfies the protocol — at type-check time via mypy or other tools. This is Python's answer to static duck typing.

The interview trick: when asked 'How does Python handle interfaces?' you can say: 'Python uses duck typing at runtime, ABCs for explicit contracts, and Protocols for static structural typing. I pick ABCs when I need runtime enforcement, Protocols when I want static checking without forcing inheritance.'

io/thecodeforge/oop/abc_protocol.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from abc import ABC, abstractmethod
from typing import Protocol

# ABSTRACT BASE CLASS
class Serializable(ABC):
    @abstractmethod
    def to_json(self) -> str:
        pass

    @abstractmethod
    def from_json(self, data: str) -> None:
        pass

class User(Serializable):
    def __init__(self, name: str):
        self.name = name

    def to_json(self) -> str:
        return f'{{"name": "{self.name}"}}'

    def from_json(self, data: str) -> None:
        import json
        self.name = json.loads(data)["name"]

# PROTOCOL for static structural typing
class Drawable(Protocol):
    def draw(self) -> None:
        ...

class Circle:
    def draw(self) -> None:
        print("Draw circle")

class Square:
    def draw(self) -> None:
        print("Draw square")

def render(obj: Drawable) -> None:
    obj.draw()

# Both work without inheriting Drawable
render(Circle())
render(Square())
Output
Draw circle
Draw square
Interview Tip:
When discussing ABCs vs Protocols, say: 'ABCs enforce a contract at runtime — a subclass must implement all abstract methods or it can't be instantiated. Protocols enforce at static analysis time — any object with matching methods satisfies the protocol, no inheritance needed. I choose ABCs when the contract is central to my domain (e.g., all payment gateways must authorize and capture). I choose Protocols when I want to accept any object that behaves a certain way without forcing a class hierarchy (e.g., anything that can draw()).'
Production Insight
A microservice used duck typing for a 'cache' object — changed to RedisCache which lacked a flush_all() method, causing silent data corruption for 2 hours.
Protocols caught the missing method at CI time when a developer refactored the cache interface.
Rule: for any public API, prefer ABCs or Protocols over bare duck typing to catch errors before production.
Key Takeaway
ABCs enforce contracts at runtime.
Protocols enforce at type-check time — no inheritance needed.
Duck typing gives flexibility but can cause runtime surprises.
Choose the one that matches your error tolerance.
ABC vs Protocol vs Duck Typing Decision
IfDo you need runtime enforcement that a subclass implements all required methods (fail-fast on instantiation)?
UseUse Abstract Base Class (ABC). The subclass cannot be created until all abstract methods are defined.
IfDo you want static type checking but allow arbitrary classes that implement the interface without inheritance?
UseUse Protocol. Mypy or pyright will check at analysis time, but no runtime constraint.
IfIs the code written for quick scripts or dynamic environments with minimal type tooling?
UseUse Duck Typing. Just call the methods and handle AttributeError if needed. No formal contract.
IfAre you building an API/library where callers should explicitly derive from your base class?
UseUse ABC. It communicates intent: 'derive from me and implement these methods.'

Instance Variables vs Class Variables — Where Most Junior Devs Get Burned

A common disaster scenario: a shared list on a class gets mutated by one instance, and suddenly every object in the system has corrupted data. That's the difference between class variables (shared across all instances) and instance variables (unique per object). Class variables are defined directly in the class body—like Dog.species = 'Canine'. Instance variables get set inside __init__ with self.name = name. The trap: mutable class variables, like lists or dicts. When you append to self.items and items is a class variable, you're modifying the class, not the instance. Python's name resolution follows a simple chain: instance → class → parent classes. Understanding this chain prevents that 3 AM PagerDuty call where a list of 'pending tasks' is shared across all users because someone forgot to initialize it inside __init__. Always put mutable defaults inside __init__, never in the class body.

variable_trap.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge
class TaskManager:
    tasks = []  # Class variable — shared by all instances

    def __init__(self, name):
        self.name = name

    def add_task(self, task):
        self.tasks.append(task)  # Mutates the class variable

# Two instances sharing the same list:
alice = TaskManager('Alice')
bob = TaskManager('Bob')
alice.add_task('Fix login bug')
bob.add_task('Deploy hotfix')
print(alice.tasks)  # ['Fix login bug', 'Deploy hotfix']
print(bob.tasks)    # Same list, same corruption
Output
['Fix login bug', 'Deploy hotfix']
['Fix login bug', 'Deploy hotfix']
Production Trap:
Never use a mutable class variable (list, dict, set) without understanding it's shared. Write it as self.tasks = [] in __init__ to give each instance its own copy.
Key Takeaway
Mutable class variables are shared state across all instances — initialize mutable data inside __init__ to avoid silent, global corruption.

Method Overloading in Python — It Doesn't Exist, So Don't Fake It

Overloaded methods sound great: same function name, different parameters, handling multiple behaviors. But Python doesn't support traditional method overloading like Java or C++. If you define def greet(self, name) and then def greet(self, name, age), the second definition silently overwrites the first. No compile error. No warning. Just a runtime surprise. The Pythonic alternative: default arguments, variable-length args (args and *kwargs), or dispatch logic with isinstance(). Or use @singledispatchmethod from the functools module for single-argument dispatch. The deeper lesson: Python's dynamic nature means the last definition wins, and overloading is a static-typing workaround. We solve the same problem differently. When you're tempted to overload, ask: 'Am I handling multiple input types or multiple behaviors?' For types, use type hints and dispatch. For behaviors, split into separate methods. Your team's code review will thank you.

overload_vs_dispatch.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge
from functools import singledispatchmethod

class Reporter:
    @singledispatchmethod
    def log(self, arg):
        raise NotImplementedError('Unsupported type')
    
    @log.register(int)
    def _(self, arg: int):
        print(f'Int log: {arg}')
    
    @log.register(str)
    def _(self, arg: str):
        print(f'String log: {arg}')

r = Reporter()
r.log(42)    # Int log: 42
r.log('err') # String log: err
Output
Int log: 42
String log: err
Interview Hack:
If asked about method overloading, say: Python uses single dispatch. Default arguments and type-based dispatch via @singledispatchmethod are the idiomatic replacements.
Key Takeaway
You cannot overload methods by signature in Python. Use default arguments or @singledispatchmethod to handle different input types cleanly.
● Production incidentPOST-MORTEMseverity: high

The Silent super() Break: A MRO Chain Failure

Symptom
AttributeError: 'RefundService' object has no attribute 'db_connection' when calling process_refund(). The attribute was defined in a base class DatabaseConnector, but only failed when RefundService was used through a specific subclass chain involving two mixins.
Assumption
The team assumed that since each mixin called super().__init__() indirectly through their own __init__ chains, the base initializer would always run. But one mixin had a custom __init__ that forgot to call super().__init__(), breaking the MRO chain.
Root cause
In a cooperative inheritance diamond (class RefundService(MixinA, DatabaseConnector)), MixinA's __init__ did not call super().__init__(). The MRO chain stopped at MixinA, so DatabaseConnector.__init__ never executed, leaving self.db_connection unset.
Fix
Add super().__init__(args, *kwargs) as the first line of MixinA.__init__. Also added a regression test that calls super().__init__() in every mixin and verifies that the full MRO chain completes.
Key lesson
  • Every class in a multiple inheritance chain must call super().__init__() unless it's the root (inheriting only from object).
  • Use __mro__ to visualize the chain during debugging — it shows exactly which initializers will run.
  • Never assume that because a parent class exists in the MRO, its __init__ will be called — it only runs if every class in the chain before it properly delegates to super().
Production debug guideSymptom → Action: Common OOP-related runtime failures4 entries
Symptom · 01
AttributeError on an attribute that 'should exist' from parent class
Fix
Check if any class in the inheritance chain overrides __init__ without calling super().__init__(). Print ClassName.__mro__ to see the full chain, then inspect each class's __init__ for missing super() calls.
Symptom · 02
Shared mutable default argument causing data corruption across instances
Fix
Look for def __init__(self, items=[]) in the codebase. Change to items=None and initialize self.items = items if items is not None else [] inside __init__. This is a Python-specific trap that affects all OOP code.
Symptom · 03
Unexpected method resolution — wrong method being called in diamond hierarchy
Fix
Print ClassName.__mro__ and compare with expected order. Ensure all classes in the diamond use cooperative super() calls. If any class breaks the chain, MRO order may produce a different method than expected.
Symptom · 04
repr() output is useless (shows object memory address instead of details)
Fix
Define __repr__ in the class. The rule: __repr__ must be unambiguous and ideally show enough info to recreate the object. Then define __str__ for user-friendly display. Without __repr__, logging and debugging become painful.
★ OOP Debugging Quick ReferenceFast commands and fixes for common Python OOP pitfalls in interviews or production.
Missing attribute from parent after subclass init
Immediate action
Check __init__ in all parent classes for missing super().__init__()
Commands
print(ClassName.__mro__)
python -c "import inspect; inspect.getsource(ClassName.__init__)"
Fix now
Add super().__init__(args, *kwargs) as first line of __init__
Mutable default argument corruption+
Immediate action
Stop using mutable defaults; switch to None sentinel
Commands
grep -rn 'def __init__(.*=\[\|.*={}\|.*=set()' .
sed -i 's/def __init__(self, items=\[\)/def __init__(self, items=None)/g'
Fix now
Assign self.items = items if items is not None else []
repr(obj) shows '<ClassName object at 0x...>'+
Immediate action
Add __repr__ method to return a meaningful string
Commands
class ClassName: def __repr__(self): return f'ClassName(field1={self.field1!r})'
python -c "import logging; logging.warning('repr: %r', obj)"
Fix now
Implement __repr__ now, then __str__ if needed
TypeError: __init__() missing 1 required positional argument+
Immediate action
Check class signature and instantiation call
Commands
import inspect; sig = inspect.signature(ClassName.__init__)
print(f'Expected: {sig.parameters}')
Fix now
Ensure instantiation matches __init__(self, ...) arguments
Method Type Comparison
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

1
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.
2
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.
3
@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.
4
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.
5
Choose ABCs for runtime contract enforcement, Protocols for static type checking without inheritance, and duck typing only for quick scripts or when you have thorough testing to catch missing methods early.

Common mistakes to avoid

4 patterns
×

Mutable default argument in __init__

Symptom
All instances of a class share the same mutable default (e.g., list, dict). Adding an item to one instance's attribute magically appears in all other instances, causing data corruption and hard-to-find bugs.
Fix
Use def __init__(self, items=None) then inside the method set self.items = items if items is not None else []. Never use mutable defaults in function signatures.
×

Forgetting to call super().__init__() in a subclass

Symptom
AttributeError on attributes that the parent class was supposed to set. The error only appears in certain code paths when the subclass is instantiated, and it can take hours to trace back to a missing super() call.
Fix
Always include super().__init__(args, *kwargs) as the first line of your subclass __init__, unless you have an explicit reason not to. In a diamond chain, every class must call super().__init__() or the chain breaks.
×

Confusing __str__ and __repr__

Symptom
repr(obj) in logs or REPL shows unhelpful <__main__.Order object at 0x7f3a...> instead of a meaningful representation. Developers waste time inspecting objects manually.
Fix
Always define __repr__ to return an unambiguous string that could recreate the object (e.g., Order(123, 'pending')). Define __str__ for user-friendly display. If only one is defined, Python falls back to __repr__ for __str__, so __repr__ is the priority.
×

Using inheritance just to reuse a method (e.g., class PDF inherits from Printer to use format())

Symptom
Nonsensical class hierarchies that break when the parent class changes. The child inherits interface methods that don't make sense for its domain.
Fix
Use composition instead: inject a Formatter object into the PDF class. You get reusability without the tight coupling and semantic mismatch.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between a classmethod and a staticmethod in Pytho...
Q02SENIOR
Explain Python's Method Resolution Order. If class C inherits from both ...
Q03SENIOR
How does Python enforce encapsulation? What's the difference between a s...
Q04SENIOR
Implement a Singleton pattern in Python using a metaclass or the __new__...
Q05SENIOR
How do Abstract Base Classes (ABCs) differ from Interfaces in Java/C#, a...
Q01 of 05SENIOR

What 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?

ANSWER
A classmethod takes cls as the first argument, giving it access to class state and the ability to create instances — perfect for alternative constructors like Employee.from_csv(row). A staticmethod takes no implicit first argument — it's just a function namespaced inside the class for logical grouping, e.g., Employee.is_valid_salary(n). Choose classmethod when you need to build instances or access class-level attributes; choose staticmethod for pure utility functions that conceptually belong to the class.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What Python OOP concepts are most commonly tested in interviews?
02
What is the difference between __str__ and __repr__ in Python?
03
How do you implement a private attribute in Python?
04
Is Python truly object-oriented if it doesn't have true private attributes?
05
When should I use an Abstract Base Class (ABC) instead of a regular class?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Python Interview. Mark it forged?

7 min read · try the examples if you haven't

Previous
Top 50 Python Interview Questions
2 / 4 · Python Interview
Next
Python Data Structures Interview Q