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.
Warm-up: from Days 1 to 5
From memory first. If one of these takes you back to an earlier lesson, that’s the warm-up working — it found something worth another look.
1. Predict all three lines.
import json
from collections import Counter
data = json.loads('{"tags": ["a", "b", "a"], "count": 2}')
print(type(data["count"]).__name__, data["tags"][-1])
print(Counter(data["tags"]).most_common(1)[0])
try:
print(data["missing"])
except KeyError as error:
print("no key:", error)Check your prediction
int a
('a', 2)
no key: 'missing'JSON numbers come back as Python numbers, not text, and [-1] is the last
tag — which is "a" again. most_common(1) is a list of one pair, so [0]
takes the pair itself. And a KeyError names the missing key, in quotes.
2. Fix. Three days of step counts, which should total 14200.
with open("steps.txt", "w") as file:
file.write("4200\n6100\n3900\n")
total = 0
with open("steps.txt") as file:
for line in file:
total += line
print(total)Check your fix
with open("steps.txt", "w") as file:
file.write("4200\n6100\n3900\n")
total = 0
with open("steps.txt") as file:
for line in file:
total += int(line)
print(total)14200Every line of a file is text — "4200\n", newline and all. int() converts it,
and doesn’t mind the newline.
3. Write safe_int(text, default=0): it returns int(text), or the
default if text isn’t a whole number. This line should print 12 0 -1:
# def safe_int(text, default=0): ...
print(safe_int("12"), safe_int("twelve"), safe_int("", -1))Check your answer
def safe_int(text, default=0):
try:
return int(text)
except ValueError:
return default
print(safe_int("12"), safe_int("twelve"), safe_int("", -1))12 0 -1Try the conversion; if it raises the one exception you expect, return the
fallback. int("") raises ValueError too.
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)A mutable class attribute is shared, not copied
species above is a string, and strings cannot be changed, so sharing one is
harmless. A list, dictionary or set in the class body is another matter:
there is exactly one of it, and changing it through any instance changes it for
every instance at once — the same one-object-shared-by-everyone shape as the
mutable default argument on Day 4.
class Dog:
tricks = [] # one list, shared by every Dog
def __init__(self, name):
self.name = name
rex = Dog("Rex")
fido = Dog("Fido")
rex.tricks.append("roll over") # meant for Rex alone...
print("rex: ", rex.tricks)
print("fido:", fido.tricks) # ...but Fido knows it too
print("Dog: ", Dog.tricks) # because it was the class's list all along
fido.tricks = ["beg"] # assigning is not mutating
print("fido:", fido.tricks) # a new list, belonging to fido alone
print("Dog: ", Dog.tricks) # the shared list is untouchedThat last pair of lines is what makes the bug hard to spot. fido.tricks.append(...)
reaches through to the class’s list, while fido.tricks = [...] creates a new
instance attribute that hides it — so the same attribute name behaves one way
when you change the list and another way when you replace it.
The cure is the one from Day 4: give every instance its own.
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # a fresh list, built once per dog
rex = Dog("Rex")
fido = Dog("Fido")
rex.tricks.append("roll over")
print("rex: ", rex.tricks)
print("fido:", fido.tricks)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 ownBefore you write your own
Predict. Two tallies, built from one class. Do they share anything?
class Tally:
def __init__(self, name):
self.name = name
self.count = 0
def add(self, n=1):
self.count += n
return self.count
a = Tally("cups")
b = Tally("steps")
a.add()
a.add(2)
print(a.count, b.count)
print(b.add(10), a.name)Check your prediction
3 0
10 cupsEach instance gets its own count, set up in __init__, so adding to a
leaves b at zero. add both changes the attribute and returns it.
Fix. This should print Ada has 150. It crashes in add.
class Wallet:
def __init__(self, owner, amount):
self.owner = owner
amount = amount
def add(self, extra):
self.amount += extra
wallet = Wallet("Ada", 100)
wallet.add(50)
print(f"{wallet.owner} has {wallet.amount}")Check your fix
class Wallet:
def __init__(self, owner, amount):
self.owner = owner
self.amount = amount
def add(self, extra):
self.amount += extra
wallet = Wallet("Ada", 100)
wallet.add(50)
print(f"{wallet.owner} has {wallet.amount}")Ada has 150amount = amount assigned the parameter to itself — a local name that vanished
when __init__ returned. Only self.something = ... puts data on the object.
The error, 'Wallet' object has no attribute 'amount', is true: it never got
one.
A 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.
When it works, the last line prints 2 75.49000000000001 — Day 1’s float story
again, since 49.99 has no exact binary form. round(cart.total(), 2) makes it
75.49.
pass below means “nothing here” — Python needs a body after each colon, so it
holds the place until you write one. Replace it as you fill each method in.
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())Hint 1: the idea
The cart needs somewhere to keep its items: a list, created in __init__ on
self, so that every cart has its own. Each method then works on that list —
add appends, total adds up the prices, count counts. The negative-price
check goes at the top of add, before anything is stored.
Hint 2: a first step
Put self.items = [] in __init__, and in add, append the pair:
self.items.append((name, price)). Then count is just the length of
self.items. Get count() printing 2 before you start on 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.
- Anything mutable that each instance should own belongs in
__init__. - A leading underscore marks something as internal by convention.