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__.