Skip to content

Python · Object-Oriented Programming

Python Classes and Instances Explained — __init__ vs __new__, self, and Attributes

4 min read Updated 2026-06-19 Share:

Practice Classes, Instances & __init__ interview questions

Python classes, explained

Classes are the foundation of OOP in Python, and the basics — self, __init__, the class/instance attribute split — come up in nearly every interview. This guide builds the mental model from "what is an instance" up to what actually happens when you call ClassName().

Class vs instance

A class is a blueprint; an instance is a concrete object built from it, with its own state:

class Dog:
    def __init__(self, name):
        self.name = name

rex = Dog("Rex")      # an instance
fido = Dog("Fido")    # a separate instance
rex.name, fido.name   # 'Rex', 'Fido' — independent state

You write the class once and create many instances, each carrying its own data but sharing the class's methods.

What self is

self is the instance the method was called on. It isn't a keyword — it's just the conventional name of the first parameter, which Python passes automatically:

class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1     # self is this particular instance

c = Counter()
c.increment()               # Python passes c as self
Counter.increment(c)        # exactly equivalent — self made explicit

So c.increment() is sugar for Counter.increment(c).

init vs new

These are often confused. __new__ creates the object; __init__ initialises the already-created object:

class Widget:
    def __new__(cls, *args):
        print("1. __new__ allocates")
        return super().__new__(cls)     # returns the new instance
    def __init__(self, size):
        print("2. __init__ configures")
        self.size = size

Widget(10)   # prints 1 then 2

__new__ runs first, is a static method receiving the class, and returns the instance. __init__ runs second, receives that instance as self, and must return None. You rarely override __new__ — it's mainly for immutable types and singletons.

Instance vs class attributes

A class attribute lives on the class and is shared by all instances; an instance attribute lives on self and is per-object. Lookup checks the instance first, then 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.species = "wolf"       # creates an INSTANCE attr that shadows the class one
b.species                # still 'Canis familiaris'

The trap: a mutable class attribute (like []) is shared and leaks state between instances — initialise mutable state in __init__.

repr vs str

__repr__ is the unambiguous, developer-facing representation (shown in the REPL and containers); __str__ is the readable, user-facing one used by print(). 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})"
    def __str__(self):
        return f"({self.x}, {self.y})"

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 want a distinct friendly form.

What happens when you call ClassName()

ClassName(args) triggers two steps via the metaclass: call __new__(cls, args) to allocate the object, then — if __new__ returned an instance of cls — call __init__(instance, args) to initialise it, and return the instance:

class Demo:
    def __new__(cls, *a):
        print("__new__")
        return super().__new__(cls)
    def __init__(self, *a):
        print("__init__")

d = Demo()    # __new__ then __init__

If __new__ returns something that isn't an instance of the class, __init__ is skipped entirely.

Recap

A class is a blueprint; instances are objects built from it with independent state. self is just the first parameter Python auto-binds to the instance. __new__ creates the object and __init__ initialises it. Class attributes are shared (watch mutable ones); instance attributes are per-object and shadow class attributes on assignment. Always define __repr__ (developer-facing) and add __str__ for a friendly form. Calling ClassName() runs __new__ then __init__.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel