Classes, Instances & __init__ Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on classes vs instances, __init__ vs __new__, the self parameter, instance vs class attributes, __repr__ vs __str__, and the object creation flow.
A class is a blueprint — it defines the attributes and methods that objects of that type will have. An instance is a concrete object built from that blueprint, with its own state. You write the class once and create many instances from it.
class Dog: # the blueprint
def __init__(self, name):
self.name = name # per-instance state
rex = Dog("Rex") # an instance
fido = Dog("Fido") # a separate instance
rex.name # 'Rex'
fido.name # 'Fido' — independent state
type(rex) # <class '__main__.Dog'>
The class itself is also an object (of type type). Each instance carries its
own data but shares the class's methods. Think of the class as the cookie
cutter and the instances as the cookies.
__new__ creates and returns the new object; __init__ initializes
that already-created object. __new__ runs first and is a static method that
receives the class; __init__ runs second and receives the instance
(self) it should configure. __init__ must return None.
class Widget:
def __new__(cls, *args):
print("__new__ — allocating")
return super().__new__(cls) # returns the instance
def __init__(self, size):
print("__init__ — configuring")
self.size = size # sets state on self
w = Widget(10) # prints __new__ then __init__
You rarely override __new__ — it's mainly for immutable types (subclassing
int/str/tuple), singletons, or metaclass tricks. For everyday classes,
just use __init__.
self is the instance the method was called on — it's how a method accesses
that object's attributes and other methods. It isn't a keyword; it's just the
conventional name of the first parameter. Python passes the instance
automatically when you call obj.method().
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1 # self refers to this instance
c = Counter()
c.increment() # Python passes c as self
Counter.increment(c) # exactly equivalent — self is explicit here
So c.increment() is sugar for Counter.increment(c). The explicitness is
deliberate — Python makes the instance visible rather than hiding it like
this in other languages.
A class attribute is defined in the class body and shared by every
instance; an instance attribute is set on self (usually in __init__)
and is unique per object. Attribute lookup checks the instance first, then
falls back to the class.
class Dog:
species = "Canis familiaris" # class attribute — shared
def __init__(self, name):
self.name = name # instance attribute — per-object
a, b = Dog("Rex"), Dog("Fido")
a.species # 'Canis familiaris' (from the class)
a.name, b.name # 'Rex', 'Fido' (independent)
a.species = "wolf" # creates an instance attr that SHADOWS the class one
b.species # still 'Canis familiaris'
Watch the classic trap: a mutable class attribute (like []) is shared and
will leak state between instances — initialize mutable state in __init__.
__repr__ is the unambiguous, developer-facing representation — ideally
something that could recreate the object — and is what you see in the REPL and
in containers. __str__ is the readable, user-facing string used by
print() and str(). If __str__ is missing, Python falls back to
__repr__.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})" # for developers
def __str__(self):
return f"({self.x}, {self.y})" # for users
p = Point(1, 2)
print(p) # (1, 2) — __str__
repr(p) # 'Point(x=1, y=2)' — __repr__
[p] # [Point(x=1, y=2)] — containers use __repr__
Rule of thumb: always define __repr__; add __str__ only when you need a
distinct friendly form.
Calling ClassName(args) invokes the class's metaclass __call__, which
orchestrates two steps: it calls __new__(cls, args) to allocate the
object, then — if __new__ returned an instance of cls — calls
__init__(instance, args) to initialize it, and finally returns the
instance.
class Demo:
def __new__(cls, *a):
print("1. __new__")
return super().__new__(cls)
def __init__(self, *a):
print("2. __init__")
d = Demo() # prints: 1. __new__ then 2. __init__
# 3. d is now bound to the fully initialized instance
Key subtlety: if __new__ returns an object that is not an instance of the
class, __init__ is skipped entirely. For normal classes you never see
this machinery — you just call the class and get back a ready object.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.