Classes, instances, and __init__¶
A class is a blueprint. It says "objects of this kind have these attributes and these methods." An instance is a specific object built from that blueprint. Every time you call the class — Counter(), Account(), MyType() — Python builds a fresh instance.
This first notebook covers just enough to make a useful class: the class keyword, __init__, what self actually is, and how methods work. Later notebooks layer on dunder methods, data classes, and inheritance.
A minimal class¶
The smallest possible class definition is a name and pass. It does nothing, but it gives you a type you can create instances of.
class Counter:
pass
c = Counter()
print(c)
print(type(c))
Two things happened. The class Counter: block defined a new type called Counter. Then Counter() called that type like a function and gave us back a new instance.
The instance prints as something like <__main__.Counter object at 0x...>. That hex address is the memory location — useful for distinguishing two instances apart, useless for anything else. We'll fix the ugly representation in the dunder methods notebook with __repr__.
Instances hold attributes¶
An attribute is just a value attached to an instance under a name. You can attach attributes from outside the class with normal assignment:
c.count = 0
c.count += 1
c.count += 1
print(c.count)
That works, but it's a bad pattern in real code. The class itself should set up its attributes when an instance is created — otherwise every caller has to remember to initialise them, and you end up with instances in inconsistent states. The mechanism for this is __init__.
__init__ and self¶
__init__ is a method that Python calls automatically every time you create an instance. Inside it, you set up the instance's attributes. The first parameter is conventionally called self and refers to the instance being built.
class Counter:
def __init__(self, start=0):
self.count = start
c1 = Counter()
c2 = Counter(start=10)
print(c1.count, c2.count)
Notice that you call Counter(start=10) — you don't pass self. Python passes the new instance as self for you. The arguments you pass in the call go to the other parameters of __init__.
What self actually is¶
There's no magic in self. It's just a normal parameter; Python passes the instance as the first argument when you call a method via the dot syntax. These two calls are equivalent:
class Greeter:
def __init__(self, name):
self.name = name
def hello(self):
return f"Hello, {self.name}!"
g = Greeter("Ada")
print(g.hello())
print(Greeter.hello(g))
Both call the same method with the same instance. The dot form g.hello() is the one you'll always use; the explicit form Greeter.hello(g) shows that self is just the first positional argument. The name self is convention only — Python doesn't care, but every Python programmer expects it, so use it.
Methods¶
Methods are functions defined inside a class. They take self as their first parameter and access instance attributes through it.
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, by=1):
self.count += by
def reset(self):
self.count = 0
c = Counter()
c.increment()
c.increment(by=5)
print(c.count)
c.reset()
print(c.count)
Each method modifies self.count directly. Notice that increment and reset don't return anything — they mutate the instance in place. That's fine for methods whose whole job is to change state. Methods that compute a value should return it explicitly.
A more realistic example¶
Here's a small bank account class — enough state and behaviour to feel like real code, small enough to fit in your head.
class Account:
def __init__(self, owner, opening_balance=0):
self.owner = owner
self.balance = opening_balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit amount must be positive")
self.balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
acct = Account("Ada", opening_balance=100)
acct.deposit(50)
acct.withdraw(30)
print(f"{acct.owner}: £{acct.balance}")
Two things to notice:
- The class enforces invariants. Outside code can't set the balance to a negative number through the documented interface —
withdrawraises rather than allowing it. (We'll see in validate attributes on assignment how to enforce this even when callers setacct.balancedirectly.) - The data and behaviour live together. To find out what an
Accountcan do, you read one class. To achieve the same with separate functions and dicts, you'd hunt through several modules.
Attributes can be set anywhere — but shouldn't be¶
Python lets you add attributes to an instance at any time. This is sometimes useful but usually a mistake — it makes the class's interface fuzzy and hard to reason about.
acct.nickname = "Rainy day fund" # works, but where is this documented?
print(acct.nickname)
Convention: set every attribute in __init__, even if its initial value is None. That way, reading the class definition tells you everything an instance carries. Data classes (notebook 03) make this even more explicit.
Exercise¶
Define a Rectangle class with:
- An
__init__that takeswidthandheight. - An
area()method that returns width times height. - A
scale(factor)method that multiplies both sides byfactorin place.
Test it by creating a 3×4 rectangle, printing its area, scaling by 2, and printing the area again.
# Your code here
Solution
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def scale(self, factor):
self.width *= factor
self.height *= factor
r = Rectangle(3, 4)
print(r.area()) # 12
r.scale(2)
print(r.area()) # 48
Recap¶
- A
classdefines a type; calling it builds an instance. __init__runs automatically when an instance is created. Use it to set up every attribute the instance will need.selfis the instance — Python passes it as the first argument to methods automatically.- Methods are just functions that take
selfand operate on it.
Next: Dunder methods, where we make a class feel like a built-in by implementing __repr__, __eq__, ordering, and friends.