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__orClassName.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 callingsuper().__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 -> True38. 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__: False39. 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 byprint().- 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.