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.
By default each instance keeps its attributes in a per-object dictionary,
__dict__. Setting self.x = 1 writes into that dict; this is why you can add
attributes dynamically at runtime.
class P:
def __init__(self):
self.x = 1
p = P()
p.__dict__ # {'x': 1}
p.y = 2 # dynamically add an attribute
p.__dict__ # {'x': 1, 'y': 2}
vars(p) # same as p.__dict__
Rule of thumb: instance attributes live in __dict__ — flexible, but it costs
memory; use __slots__ to remove it when you have many small objects.
A @classmethod receives the class as its first argument (cls) — ideal for
alternative constructors. A @staticmethod receives nothing automatic — it's
a plain function grouped under the class for namespacing.
class Date:
def __init__(self, y, m, d):
self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, s): # alt constructor
return cls(*map(int, s.split("-")))
@staticmethod
def is_leap(year): # utility, no self/cls
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
Date.from_string("2026-06-19")
Date.is_leap(2024) # True
Rule of thumb: use classmethod when you need cls (factories, subclass-aware);
staticmethod for related helpers that touch neither instance nor class.
These built-ins access attributes by name string at runtime: getattr(obj, "x") reads (with an optional default), setattr(obj, "x", v) writes, hasattr
tests existence. Useful for dynamic/config-driven code.
class C: pass
c = C()
setattr(c, "speed", 5) # c.speed = 5
getattr(c, "speed") # 5
getattr(c, "missing", 0) # 0 — default avoids AttributeError
hasattr(c, "speed") # True
delattr(c, "speed") # remove it
Rule of thumb: reach for getattr/setattr when the attribute name is computed or
data-driven; otherwise use plain dot access.
No — __init__ must return None. Returning anything else raises TypeError.
Its job is to mutate self in place, not to produce the object (that's __new__'s
role).
class Bad:
def __init__(self):
return 42 # TypeError: __init__ should return None
class Good:
def __init__(self, x):
self.x = x # configure self, return nothing
Rule of thumb: __init__ sets up state on self and implicitly returns None; if
you need to control what object comes back, override __new__.
Defining __eq__ sets __hash__ to None, making instances unhashable
(can't go in sets/dict keys) unless you also define __hash__. Python does this
because equal objects must have equal hashes.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # restore hashability, consistent with __eq__
{Point(1, 2)} # works only because __hash__ is defined
Rule of thumb: if you implement __eq__ and need the object hashable, implement a
consistent __hash__ over the same fields — or use @dataclass(frozen=True).
A leading double underscore (no trailing) triggers name mangling: inside
class C, self.__x becomes self._C__x. It's not true privacy but avoids
accidental clashes in subclasses.
class Base:
def __init__(self):
self.__secret = 1 # stored as _Base__secret
b = Base()
b.__secret # AttributeError
b._Base__secret # 1 — accessible if you know the mangled name
Rule of thumb: use single _name for "internal, please don't touch"; reserve __name
mangling for attributes you must protect from subclass name collisions.
Accessing a method through an instance gives a bound method — the instance is
pre-bound as self. Accessing it through the class gives the plain function, so
you must pass the instance explicitly.
class C:
def greet(self): return "hi"
c = C()
c.greet # <bound method C.greet of <C object>>
c.greet() # "hi" — self is c automatically
C.greet # <function C.greet> — plain function
C.greet(c) # "hi" — pass self manually
m = c.greet; m() # "hi" — bound method remembers c
Rule of thumb: instance.method captures the instance (a bound method you can store
and call later); Class.method is just the function.
A class is an instance of its metaclass (normally type). So classes are
first-class objects: you can assign them to variables, pass them to functions, store
them in lists, and even create them at runtime with type(name, bases, dict).
class A: pass
type(A) # <class 'type'> — A is an instance of type
isinstance(A, object) # True
registry = {"a": A} # store classes like any value
Dynamic = type("Dynamic", (), {"x": 1}) # build a class at runtime
Dynamic().x # 1
Rule of thumb: classes are objects produced by type; treating them as values
enables factories, registries, and metaclass-based frameworks.
__del__ is a finalizer called when an object is about to be garbage-collected —
not a deterministic destructor. Its timing is unpredictable (especially with
reference cycles), exceptions in it are ignored, and it may not run at all at
interpreter exit.
class Resource:
def __del__(self):
print("cleanup") # runs whenever GC decides — maybe never
r = Resource(); del r # may or may not print immediately
Rule of thumb: don't rely on __del__ for cleanup — use context managers
(__enter__/__exit__) or explicit close(); reach for __del__ only as a
last-resort safety net.
More Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.