OOP in Python: Classes, Inheritance, the MRO Diamond and Dunder Methods for Interviews

Work through Python's MRO diamond and the __eq__ plus __hash__ rule. These two examples explain many of the OOP output questions asked in interviews.

KnowledgeGate Team

Exam prep & CS education

Updated 15 Jul 20265 min read

Python OOP interviews rarely stop at "What is a class?" They ask which method runs in a diamond, why two objects compare equal but cannot go into a set, or what super() actually calls. Each answer follows a language rule, not personal style.

Once those rules are clear, output questions become traces. You inspect the instance, walk the method resolution order, and apply the relevant dunder method.

Classes, instances and self

A class defines data and behaviour. An instance is one object created from that class. In an instance method, self refers to the instance on which the method was called, and Python passes it automatically.

class Student:
    school = "KnowledgeGate"

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

    def introduce(self):
        return f"I am {self.name}"

Student.school is a class attribute, shared through the class. self.name is an instance attribute, stored separately on each student object.

__init__ is an initialiser. By the time it runs, Python has already created the instance. That is why calling it a constructor, in the strict C++ or Java sense, is imprecise. Object creation is handled by __new__; ordinary application code usually customises __init__.

The classic class-attribute trap uses a mutable value:

class Box:
    tags = []

a = Box()
b = Box()
a.tags.append(1)

print(b.tags)  # [1]

Both instances reach the same list through the class. If every object needs its own list, assign self.tags = [] inside __init__.

Inheritance and what super() follows

Inheritance lets a specialised class reuse and override behaviour from a base class:

class Animal:
    def speak(self):
        return "sound"

class Dog(Animal):
    def speak(self):
        return "bark"

Dog inherits from Animal, but its own speak() wins because attribute lookup finds it first.

The subtle rule is that super() does not simply mean "call my parent". It means "continue from the current class to the next class in this instance's method resolution order." With single inheritance, those ideas often look identical. Multiple inheritance exposes the difference.

Cooperative classes call super() consistently:

class Named:
    def __init__(self, name, **kwargs):
        self.name = name
        super().__init__(**kwargs)

When every class in a compatible hierarchy accepts what it needs and forwards the rest, a diamond can initialise each class once along one ordered path. A class that calls a base by name can skip part of that cooperative chain or initialise a base twice.

Method Resolution Order and the diamond

Consider the classic diamond:

class A:
    def who(self):
        return "A"

class B(A):
    def who(self):
        return "B"

class C(A):
    def who(self):
        return "C"

class D(B, C):
    pass

D().who() returns "B".

Python uses C3 linearisation to build a consistent method resolution order. For this hierarchy:

D.__mro__
(D, B, C, A, object)

Python prints each entry as a full class repr, abbreviated above to the bare class names. Attribute lookup walks that tuple from left to right. D has no who. B does, so lookup stops and returns B's method without reaching C or A.

The compact derivation is:

L[D] = D + merge(L[B], L[C], [B, C])
     = D, B, C, A, object

The merge preserves local parent order, so B stays before C because class D(B, C) declared it that way. It also preserves monotonicity, meaning a subclass does not reverse an ordering already established in its parents. That is why MRO is more than a simple depth-first walk.

Diamond inheritance of classes A, B, C and D with the MRO path D, B, C, A, object, showing why D().who() resolves to B.

Print ClassName.__mro__ whenever a multiple-inheritance question feels ambiguous. The tuple is the rule Python will actually follow. The broader OOP concepts guide for CS exams helps connect this Python-specific mechanism to inheritance, polymorphism, abstraction, and encapsulation.

Dunder methods that carry interview weight

Dunder methods, named with double underscores, connect your class to Python's object model.

  • __repr__ should give a useful, unambiguous developer-facing representation.

  • __str__ should give a readable user-facing form. If it is absent, str(obj) falls back to __repr__.

  • __len__ supports len(obj).

  • __call__ lets an instance be called like a function.

  • __getitem__ supports indexed or key-based access.

Equality and hashing are a particularly important pair:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

Now Point(1, 2) == Point(1, 2) is True. However, defining value equality without a compatible __hash__ makes this class unhashable. hash(Point(1, 2)) raises TypeError, so the object cannot be a set member or dictionary key.

For an effectively immutable point, a compatible implementation is:

def __hash__(self):
    return hash((self.x, self.y))

Equal points then produce equal hashes because both methods use the same fields. Do not mutate those fields while an object is stored in a set or used as a dictionary key. If points are meant to be mutable, leaving them unhashable is safer.

classmethod, staticmethod and property

These decorators describe how a method binds:

  • @classmethod receives cls and is useful for alternative constructors such as Point.from_tuple((1, 2)).

  • @staticmethod receives neither self nor cls. It is a related plain function kept in the class namespace.

  • @property exposes a method through attribute syntax, which is useful for a computed or validated value.

A property without a setter is read-only through normal assignment. It can compute a value each time while keeping the public interface as simple as obj.value.

Traps interviewers set

Four traps recur because each reveals whether you know the rule underneath:

  1. A mutable class attribute is shared across instances.

  2. Defining __eq__ without a compatible __hash__ removes hashability.

  3. Skipping super().__init__() can leave a base class uninitialised.

  4. is checks identity, while == checks value equality. Two empty lists compare equal, but they are distinct objects.

Interviewers also ask you to predict a method from an MRO, distinguish __str__ from __repr__, or write a small value class with correct equality. Our question bank carries over 450 Python practice questions and close to 200 language-independent OOP questions covering these class, inheritance, and dunder patterns. The Python interview questions hub gives the surrounding freshers-level question map.

The short version and next step

self is the instance. super() continues through the MRO, not merely to a class you informally call the parent. In D(B, C), the diamond resolves through D, B, C, A, and object. Defining __eq__ without __hash__ also costs an object set membership.

Build the language foundation in the Python Programming course, then practise the interview layer with DSA Using Python. The Coding and DSA courses section places Python beside C, C++, Java, and the data-structures practice the next interview round tests.