So far you have used values that Python provides: numbers, text, lists and dictionaries. A class lets you create your own kind of value that bundles data and the actions that go with it. A bank account has a balance and can deposit and withdraw. A student has marks and can work out a grade. When data and actions belong together, a class keeps them together and makes your code easier to understand. This is called object-oriented programming, and in this lesson you meet it gently.
Class and object
A class is a blueprint. An object (or instance) is something made from that blueprint. If Wallet is the class, then one person's wallet is an object, and another person's wallet is another object. Each object has its own data, called attributes, and its own actions, called methods. Read this program and its output.
from dataclasses import dataclass
class Wallet:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.history = []
def add(self, amount, note=""):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
self.history.append(("add", amount, note))
def spend(self, amount, note=""):
if amount > self.balance:
raise ValueError("Not enough money")
self.balance -= amount
self.history.append(("spend", amount, note))
def __str__(self):
return f"{self.owner}: balance {self.balance}"
w = Wallet("Asha", 500)
w.add(200, "salary bonus")
w.spend(150, "groceries")
print(w)
print(len(w.history), w.history[-1])
try:
w.spend(10_000)
except ValueError as error:
print("Blocked:", error)
@dataclass
class Expense:
category: str
amount: float
paid: bool = False
e = Expense("Food", 250.0)
print(e)
print(e.amount, e == Expense("Food", 250.0))
class Savings(Wallet):
def add_interest(self, rate):
self.add(round(self.balance * rate, 2), "interest")
s = Savings("Ravi", 1000)
s.add_interest(0.05)
print(s)
Asha: balance 550
2 ('spend', 150, 'groceries')
Blocked: Not enough money
Expense(category='Food', amount=250.0, paid=False)
250.0 True
Ravi: balance 1050.0
Understanding the class
__init__is the constructor. Python runs it when you writeWallet("Asha", 500). Its first parameter,self, is the object that is being made.self.owner = ownerstores the value on that object.- Methods are functions inside the class, and they also take
selfas the first parameter.w.add(200, "salary bonus")is the same as saying: use theaddmethod of the objectw. Theselfis filled in for you. - The methods check their input.
spendraises aValueErrorwhen you try to spend more than the balance, and the code outside catches it. The class protects its own data from invalid changes. __str__tells Python how to turn the object into text forprint. Without it, you would see an unhelpful text with a memory address.- Every object has its own data. Changing one wallet does not change another.
Dataclasses: classes for simple records
Often a class only holds data, like a row of a table. Writing __init__ by hand for that is boring. The @dataclass decorator from the standard library writes it for you from a list of fields with types. It also gives you a readable text form and a == comparison for free, as the output shows: Expense(category='Food', amount=250.0, paid=False), and two expenses with the same values are equal. The = False gives a default value. Dataclasses are a great way to make your data clear.
Inheritance
A class can inherit from another. class Savings(Wallet) means: a Savings wallet is a Wallet with everything Wallet has, plus whatever it adds, here add_interest. We created Savings("Ravi", 1000) and added 5 percent interest, so the balance became 1050.0. Use inheritance when one thing is truly a special kind of another thing. Do not use it only to share a few lines of code. Many experienced programmers prefer to keep the structure simple and use inheritance rarely.
When should I use a class?
- When you have data and several functions that always work on that same data.
- When you have many similar things, such as many students or many orders, each with its own state.
- When a simple dictionary has grown complicated, and you want clear names and checks.
- You do not need a class for everything. A few plain functions are often the best solution.
Naming and style
- Class names use
CapitalWords:Wallet,Expense. Function and variable names uselowercase_with_underscores. - Attributes that are for internal use only start with an underscore, like
self._cache. It is a hint to other programmers, and not a lock. - Write a short docstring for the class and for each method that is not obvious.
Common mistakes
- Forgetting
selfas the first parameter of a method gives aTypeErrorabout the number of arguments. - Forgetting
self.when you set or read an attribute:balance = 5inside a method makes only a local variable. - Using a mutable default value such as
history=[]in__init__. Create the list inside, as the example does withself.history = [].
Your task
- Write a
Studentclass withnameand a list ofmarks. Add a methodaverage()and a methodgrade()that uses the average, with the same rules as lesson 3. - Add
__str__so thatprint(student)shows the name and the average. - Make a
Productdataclass withname,priceandstock, and a function that returns the products that are low on stock. - Make
Walletraise an error if someone creates it with a negative starting balance.
How to check it worked
- A student with marks
[80, 90, 70]has average80.0, and gradeB. print(student)shows readable text, not a memory address.- Your
Wallet("Asha", -5)raises aValueError.