Two things about __init__ and self surprise people who came from another language, and both are easier once you see what Python is actually doing.
self is a parameter, not a keyword
When you call d.speak(), Python calls Dog.speak(d). The instance is passed as the first argument, and self is just the name we give it.
# self is a parameter name, not a keyword
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says woof"
d = Dog('Rex')
print(d.speak())
print(Dog.speak(d)) # the same call, written out
It prints:
Rex says woof
Rex says woof
Both lines are the same call. d.speak() is shorthand.
You can prove self is only a convention:
# proof that self is only a convention
class Cat:
def __init__(whatever, name):
whatever.name = name
def speak(this):
return f"{this.name} says meow"
print(Cat('Mia').speak())
It prints:
Mia says meow
That works. Never write it — every Python reader expects self, and breaking that costs more than it could possibly gain. But knowing it is a parameter explains why you have to write it in every method signature, which otherwise looks like noise.
__init__ does not create the object
The name suggests a constructor. It is not one. By the time __init__ runs, the object already exists — __init__ only fills it in.
# __init__ does not create the object, it fills one in
class Tracked:
def __new__(cls, *args):
print(' __new__ ran, object does not exist yet')
return super().__new__(cls)
def __init__(self, value):
print(' __init__ ran, self already exists:', type(self).__name__)
self.value = value
t = Tracked(5)
print('value =', t.value)
It prints:
__new__ ran, object does not exist yet
__init__ ran, self already exists: Tracked
value = 5
__new__ is the actual constructor. You will almost never write one — it matters for immutable types and metaclasses, and nowhere else. But it explains the naming: __init__ initialises, it does not create.
Class attributes are shared
An attribute assigned in the class body belongs to the class. One exists, no matter how many instances you make.
# instance attributes vs class attributes
class Counter:
total = 0 # shared by every instance
def __init__(self):
self.count = 0 # one per instance
def tick(self):
self.count += 1
Counter.total += 1
a, b = Counter(), Counter()
a.tick(); a.tick(); b.tick()
print('a.count =', a.count, '| b.count =', b.count, '| Counter.total =', Counter.total)
It prints:
a.count = 2 | b.count = 1 | Counter.total = 3
count is per instance. total is one number for the whole class.
The part that trips people
Reading a class attribute through an instance works. Assigning through an instance does not update the class — it creates a new instance attribute that shadows it.
# assigning through the instance shadows the class attribute
a.total = 99
print('a.total =', a.total, '| b.total =', b.total, '| Counter.total =', Counter.total)
print('a has its own:', 'total' in a.__dict__, '| b does not:', 'total' in b.__dict__)
It prints:
a.total = 99 | b.total = 3 | Counter.total = 3
a has its own: True | b does not: False
a now has its own total. b and the class still share the original. This is why Counter.total += 1 in tick is written against the class and not self.total += 1 — the latter would read the class value, add one, and quietly store the result on the instance.
The same rule is why a mutable class attribute is a trap. A list in the class body is shared by every instance, and self.items.append(x) mutates the shared list rather than shadowing it. Assign mutable attributes in __init__.
What to remember
-
d.speak()isDog.speak(d).selfis the first parameter, named by convention. -
__init__initialises an object that already exists.__new__creates it, and you will rarely write one. -
A class attribute is shared. Reading through an instance finds it; assigning through an instance shadows it.
-
Mutable class attributes are shared and mutable — put them in
__init__.