You have been using objects since Day 1. "hello".upper() calls a method on a
string object; [1, 2].append(3) calls one on a list object. Today you make
your own.
The problem classes solve
Here is a program written with the tools you already have:
account_owner = "Ada"
account_balance = 100
def deposit(balance, amount):
return balance + amount
account_balance = deposit(account_balance, 50)
print(account_owner, account_balance)
# Now add a second account, and a third...
other_owner = "Alan"
other_balance = 20Every new account needs another pair of variables, and nothing links a balance to its owner. A class ties them together.
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
ada = Account("Ada", 100)
alan = Account("Alan", 20)
ada.deposit(50)
print(ada.owner, ada.balance)
print(alan.owner, alan.balance)Two independent accounts, each carrying its own data and the behaviour that belongs to it.
The vocabulary
- A class is the blueprint:
Account. - An instance is one thing built from it:
ada. - Attributes are the data on an instance:
ada.balance. - Methods are functions defined in the class:
ada.deposit(50).
__init__ and self
__init__ runs automatically when you create an instance. It is where
attributes are set up.
class Book:
def __init__(self, title, pages):
print(f" building a Book: {title}")
self.title = title
self.pages = pages
self.page_read = 0 # attributes not from arguments are fine too
def read(self, pages):
self.page_read = min(self.page_read + pages, self.pages)
return self.progress()
def progress(self):
return f"{self.page_read}/{self.pages} pages"
dune = Book("Dune", 412)
print(dune.progress())
print(dune.read(100))
print(dune.read(400))self is the instance the method was called on. Python passes it
automatically: writing dune.read(100) calls read(dune, 100). Every method
takes self first — forget it and you get
TypeError: read() takes 1 positional argument but 2 were given.
self is a convention, not a keyword
You could call it anything. Nobody does. Call it self and every Python
programmer will understand your code instantly.
Instance attributes versus class attributes
An attribute defined in the class body is shared by every instance. One defined
on self belongs to that instance alone.
class Dog:
species = "Canis familiaris" # class attribute - shared
def __init__(self, name):
self.name = name # instance attribute - personal
rex = Dog("Rex")
fido = Dog("Fido")
print(rex.name, fido.name)
print(rex.species, fido.species)
Dog.species = "Canis lupus familiaris" # change it once, everyone sees it
print(rex.species, fido.species)Never share a mutable class attribute
tricks = [] in the class body gives every dog the same list, exactly like
the mutable default argument trap from Day 4. Create mutable attributes inside
__init__.
Methods that guard the data
The point of putting behaviour beside data is that the class can enforce its own rules.
class Account:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("balance cannot start negative")
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError(f"insufficient funds: balance is {self.balance}")
self.balance -= amount
account = Account("Ada", 100)
account.deposit(50)
account.withdraw(30)
print(account.balance)
account.withdraw(1000)A leading underscore means “internal”
Python has no private attributes. It has a convention: a name starting with _
is an implementation detail, and other code should leave it alone.
class Timer:
def __init__(self):
self._start = 0 # internal - do not touch from outside
self.laps = [] # public
def lap(self, seconds):
self.laps.append(seconds)
def best(self):
return min(self.laps) if self.laps else None
timer = Timer()
timer.lap(12.4)
timer.lap(11.9)
print(timer.best())
print(timer._start) # possible, but you are on your ownA shopping cart
Write a Cart class with an empty list of items. Give it add(name, price),
total() returning the sum of prices, and count() returning how many items.
Adding a negative price should raise a ValueError.
class Cart:
def __init__(self):
pass # your code here
def add(self, name, price):
pass # your code here
def total(self):
return 0 # your code here
def count(self):
return 0 # your code here
cart = Cart()
cart.add("keyboard", 49.99)
cart.add("mouse", 25.50)
print(cart.count(), cart.total())Show one solution
class Cart:
def __init__(self):
self.items = []
def add(self, name, price):
if price < 0:
raise ValueError(f"price cannot be negative: {price}")
self.items.append((name, price))
def total(self):
return sum(price for _, price in self.items)
def count(self):
return len(self.items)
cart = Cart()
cart.add("keyboard", 49.99)
cart.add("mouse", 25.50)
print(cart.count(), round(cart.total(), 2))self.items = [] lives in __init__, so each cart gets its own list — the
mutable-attribute rule in practice. Each item is a (name, price) tuple, which
is Day 3’s “fixed record” advice applied.
What you learned
- A class is a blueprint; an instance is one object built from it.
__init__sets up attributes when an instance is created.selfis the instance, passed automatically as the first argument.- Class attributes are shared; instance attributes belong to one object.
- A leading underscore marks something as internal by convention.