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.
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.
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:
__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.__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__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.
__slots__ removes the automatic__dict__ (unless you include '__dict__' in slots), saving memory per instance.__slots__, which can be a helpful discipline.__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__: FalseDunder 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.
__init__ (constructor), __repr__ and __str__ (representations), __len__,__iter__, __enter__/__exit__ (context manager), __add__ (operator +), etc.len(obj), iter(obj)).__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 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().__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.
3 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.