Python Full-Stack Interview Questions 36–40 (MRO, __new__, __slots__, Dunder Methods, str vs repr)

This lesson covers five important Python topics you are likely to encounter in interviews. Each question is explained in a friendly, beginner-focused way with real-world analogies, runnable examples, and the expected terminal output so you can run them in your editor and follow along. Read deliberately and try each example.

36. What is MRO (Method Resolution Order) in Python?

MRO (Method Resolution Order) is the order Python follows to look up methods and attributes on a class when multiple inheritance is used. Think of it like a line of people you ask for directions; Python asks each class in a specific sequence until it finds the method or attribute. The MRO ensures consistency and avoids ambiguous lookups.

  • For single inheritance, MRO is trivial: check the class, then its parent, then the parent’s parent, and so on.
  • For multiple inheritance, Python uses the C3 linearization algorithm to produce a deterministic order that preserves local precedence and monotonicity.
  • You can inspect a class's MRO with ClassName.__mro__ or ClassName.mro().

Example demonstrating MRO behavior and how it affects method lookup.

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


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


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


# Multiple inheritance: Method Resolution Order (MRO) matters
class D(B, C):
    pass


class E(C, B):
    pass


# Print MRO chains
print("D MRO:", [cls.__name__ for cls in D.mro()])
print("E MRO:", [cls.__name__ for cls in E.mro()])

# Which version of 'who()' gets called?
print("D().who() ->", D().who())  # B is checked before C (D → B → C → A)
print("E().who() ->", E().who())  # C is checked before B (E → C → B → A)
 

Expected output:

# Output:

D MRO: ['D', 'B', 'C', 'A', 'object']
E MRO: ['E', 'C', 'B', 'A', 'object']
D().who() -> B
E().who() -> C 

Interview tips:

  • Show you know how to inspect MRO and explain why order changes behavior in multiple inheritance.
  • Mention C3 linearization briefly — it preserves the ordering from each base while producing a single consistent sequence.

37. What are __new__ and __init__ in Python?

__new__ and __init__ are both involved in creating and initializing objects, but they have different roles.__new__ is the method that actually creates and returns a new instance (it runs before the instance exists). __init__ receives the newly created instance and initializes its attributes. Analogy: __new__ buys the empty notebook; __init__ writes your name on the first page and fills the notebook.

  • __new__(cls, ...) is a static-like method that must return the new instance (usually by calling super().__new__(cls)).
  • __init__(self, ...) initializes attributes on the instance that __new__ returned.
  • Override __new__ when you need to control instance creation (e.g., immutable types, singletons, or customizing allocation).

Example showing normal usage vs a case that customizes __new__.

class Point:
    def __new__(cls, *args, **kwargs):
        # Create the instance (usually delegated to 'super')
        instance = super().__new__(cls)
        print("Point.__new__ called")
        return instance

    def __init__(self, x, y):
        print("Point.__init__ called")
        self.x = x
        self.y = y


# Creating a Point object
p = Point(2, 3)
print("p:", p.x, p.y)


# Example: Caching instance in __new__ (simple Singleton-like behavior)
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance


# Both variables point to the same object
a = Singleton()
b = Singleton()
print("a is b ->", a is b)

Expected output:

# Output:

Point.__new__ called
Point.__init__ called
p: 2 3
a is b -> True

38. Explain Python’s __slots__.

__slots__ is a special class attribute that, when defined, limits the attributes instances of the class can have and avoids the per-instance dynamic attribute dictionary (__dict__). Use it when you want memory savings for many small objects or to prevent arbitrary new attributes.

  • Defining __slots__ removes the automatic__dict__ (unless you include '__dict__' in slots), saving memory per instance.
  • It restricts allowed attribute names to those listed in__slots__, which can be a helpful discipline.
  • Note: __slots__ affects attribute assignment and can complicate inheritance; use when you have many instances and need the optimization.

Example comparing normal class vs class with __slots__ and showing attribute restriction.

class Normal:
    def __init__(self, x):
        self.x = x


class Slim:
    __slots__ = ("x",)  # only 'x' allowed

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


# Normal object can have dynamic attributes
n = Normal(1)
n.y = 2   # allowed, stored in __dict__


# Slim object cannot have attributes outside __slots__
s = Slim(10)
try:
    s.y = 20   # raises AttributeError
except AttributeError as exc:
    print("Error:", exc)


# Checking for __dict__
print("Normal has __dict__:", hasattr(n, "__dict__"))
print("Slim has __dict__:", hasattr(s, "__dict__"))

Expected output:

# Output:

Error: 'Slim' object has no attribute 'y'
Normal has __dict__: True
Slim has __dict__: False

39. What are Python dunder methods?

Dunder methods (double-underscore methods) are special methods with names like __init__, __str__,__add__, and __iter__. They implement Python's built-in behavior and operator protocols so your objects can interact with language features (construction, printing, arithmetic, iteration, comparison, etc.). They let you make objects behave like built-ins.

  • Examples: __init__ (constructor), __repr__ and __str__ (representations), __len__,__iter__, __enter__/__exit__ (context manager), __add__ (operator +), etc.
  • Implementing these methods improves ergonomics and allows Python to use your class naturally (e.g., len(obj), iter(obj)).
  • Be careful to follow expected semantics (e.g., __eq__ should be symmetric), and avoid surprising side-effects in dunders.

Example implementing a small Vector2 with dunder methods for addition, length, and friendly display.

import math


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

    def __add__(self, other):
        return Vector2(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector2({self.x!r}, {self.y!r})"

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

    def __len__(self):
        # Not typical, but illustrative: treat length of vector object as 2
        return 2


v1 = Vector2(1, 2)
v2 = Vector2(3, 4)

print("v1 + v2 ->", v1 + v2)
print("repr(v1) ->", repr(v1))
print("str(v1) ->", str(v1))
print("len(v1) ->", len(v1))

Expected output:

# Output:

v1 + v2 -> Vector2(4, 6)
repr(v1) -> Vector2(1, 2)
str(v1) -> (1, 2)
len(v1) -> 2 

40. Explain the difference between Python’s str and repr.

repr() and str() both return string representations, but they serve different audiences:repr() aims to be an unambiguous representation primarily for developers and debugging (often a string that could be used to recreate the object). str() aims to be a readable, user-friendly presentation. If only __repr__ is defined,str() falls back to it.

  • __repr__: developer-centric, unambiguous, useful for debugging.
  • __str__: user-centric, readable, used by print().
  • Rule of thumb: implement __repr__ first; implement __str__ when you want prettier output for users.

Example showing both representations and their default behavior.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Person(name={self.name!r}, age={self.age!r})"

    def __str__(self):
        return f"{self.name} ({self.age} years)"


p = Person("Asha", 28)
print("repr(p) ->", repr(p))
print("str(p)  ->", str(p))
print("print(p) ->", end=" ")
print(p) 

Expected output:

# Output:

repr(p) -> Person(name='Asha', age=28)
str(p)  -> Asha (28 years)
print(p) -> Asha (28 years) 

Final notes: For interviews, explain these topics with short analogies (MRO: "who to ask first", __new__ vs __init__: "buying vs filling the notebook", __slots__: "pre- labelled storage boxes", dunder methods: "special hooks", repr vs str: "developer vs user view"). Walk through a small code example if asked—interviewers value clarity, correct terminology, and a short demonstration.

🚀 Deep Dive With AI Scholar