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.