Section · 01
__init__ runs once per instance
__init__is a special method (those double underscores mean “Python calls this for you”) that runs automatically whenever you create an instance. Its job is to set up the instance’s starting state.
class BankAccount:
def __init__(self, owner, opening_balance=0):
self.owner = owner
self.balance = opening_balance
# Calling the class triggers __init__
acct = BankAccount("Ada", 100)
print(acct.owner) # "Ada"
print(acct.balance) # 100When you write BankAccount("Ada", 100):
1. Python creates a new, empty BankAccount object.
2. It calls __init__ on that object, passing it in as self.
3. __init__ assigns attributes onto self.
4. The fully-constructed object is what BankAccount(...) returns.You never call __init__ directly. You call the class — BankAccount(...) — and Python wires up the rest.
Section · 02
self is the instance
The first parameter of __init__ (and every regular method) is self. It’s a reference to the specific instance the method is being called on. Inside the method, self.something = value attaches an attribute to this instance.
class BankAccount:
def __init__(self, owner, opening_balance=0):
self.owner = owner # attach owner to this instance
self.balance = opening_balance # attach balance to this instance
a = BankAccount("Ada", 100)
b = BankAccount("Ben", 250)
a.owner # "Ada"
b.owner # "Ben"
a.balance = 999 # only changes a, not b
b.balance # still 250The name self is just convention. Python passes the instance in as the first argument no matter what you call it. Everyone uses self. Use self.
Section · 03
Instance attributes vs class attributes
You can also put attributes directly on the class — outside any method. Those are class attributes. They’re shared across every instance.
class BankAccount:
# Class attribute — shared
interest_rate = 0.045
def __init__(self, owner, opening_balance=0):
# Instance attributes — per-object
self.owner = owner
self.balance = opening_balance
a = BankAccount("Ada", 100)
b = BankAccount("Ben", 250)
a.interest_rate # 0.045 (looked up on the class)
b.interest_rate # 0.045 (same one)
# Change it on the CLASS — affects all instances
BankAccount.interest_rate = 0.05
a.interest_rate # 0.05
b.interest_rate # 0.05When to use which:
Instance attribute — different for each instance (owner, balance)
Class attribute — same for all instances (a tax rate, a config value,
a default constant)A common gotcha: assigning to a class attribute via an instance actually creates an instance attribute that shadows the class one. This trips up people who think they’re updating a shared value:
a.interest_rate = 0.10 # creates an INSTANCE attribute on a
a.interest_rate # 0.10
b.interest_rate # 0.05 — unchanged, still using the class one
BankAccount.interest_rate # 0.05 — class attribute also unchangedSection · 04
The mutable class-attribute trap
We hit a version of this in Unit 1 (mutable default arguments). The class-attribute version is sneakier:
class Inbox:
messages = [] # WRONG — shared across all instances!
def add(self, msg):
self.messages.append(msg)
a = Inbox()
b = Inbox()
a.add("hello")
print(b.messages) # ["hello"] — surprise!Both a and b point at the same list, because messages lives on the class, not the instance. Fix: initialize the mutable in __init__ so each instance gets its own:
class Inbox:
def __init__(self):
self.messages = [] # fresh list per instance
a = Inbox()
b = Inbox()
a.messages.append("hello")
b.messages # []Rule of thumb: class attributes are fine for immutable defaults (numbers, strings, tuples, booleans). For lists, dicts, sets, and any custom object — initialize in __init__.
Section · 05
Adding attributes from outside
Python doesn’t lock down what attributes an instance can have. You can add new ones from outside the class:
acct = BankAccount("Ada", 100)
acct.nickname = "Main checking" # totally legal — new attribute
print(acct.nickname)This is technically allowed, but it’s a smell. Ifnickname is a meaningful piece of an account, it should be in __init__ alongside owner and balance. Otherwise you have accounts in two shapes (some with nickname, some without), and every downstream function has to check.
The discipline: declare every attribute in __init__ — even if you set it to None initially. That way, a reader who lands in your class has one place to look to know what each instance carries:
class BankAccount:
def __init__(self, owner, opening_balance=0):
self.owner = owner
self.balance = opening_balance
self.nickname = None # explicit "not set yet"
self.last_login = NoneFuture you in six months will thank you. Future them who has to debug it definitely will.