Define a small class with an __init__ method, attributes and methods that work on the object's own data.
Why it matters
So far you have kept data (lists, dictionaries) and the functions that work on it separately. As programs grow, it helps to bundle them together: a sensor has a name, a unit and readings, and you can ask it for its average. A class describes that bundle, and each object made from it carries its own data. Everything in Python is already an object (a string has .upper(), a list has .append()), so learning classes also explains how the tools you already use work.
Defining a class
class Sensor:
def __init__(self, name, unit):
self.name = name
self.unit = unit
self.readings = []
def add(self, value):
self.readings.append(value)
def average(self):
if not self.readings:
return 0.0
return sum(self.readings) / len(self.readings)
class Sensor:starts the definition. Class names useCapWordsstyle.- Functions defined inside a class are methods.
__init__(two underscores on each side) is the initialiser. Python calls it automatically when you create a new object, to set up the object's starting data.selfis the object the method is working on. Every method takes it as its first parameter.self.name = namestores the argument as an attribute of this particular object.
Creating and using objects
tank = Sensor("tank-a", "C")
pump = Sensor("pump-1", "bar")
tank.add(20.5)
tank.add(21.5)
pump.add(3.2)
print(tank.name, tank.readings) # tank-a [20.5, 21.5]
print(tank.average()) # 21.0
print(pump.average()) # 3.2
Calling the class like a function, Sensor("tank-a", "C"), creates a new object (an instance) and runs __init__ with self set to it. When you call tank.add(20.5), Python passes tank as self for you; you never pass self yourself.
Each instance has its own attributes. Adding a reading to tank does not touch pump.
Methods that use other methods
Methods can call each other through self:
class Sensor:
def __init__(self, name, unit):
self.name = name
self.unit = unit
self.readings = []
def add(self, value):
self.readings.append(value)
def average(self):
if not self.readings:
return 0.0
return sum(self.readings) / len(self.readings)
def summary(self):
return f"{self.name}: {len(self.readings)} readings, mean {self.average():.1f} {self.unit}"
tank = Sensor("tank-a", "C")
for v in [20.5, 21.5, 22.0]:
tank.add(v)
print(tank.summary()) # tank-a: 3 readings, mean 21.3 C
A readable printout with __str__
By default, print(tank) shows something unhelpful like <__main__.Sensor object at 0x10a...>. Define __str__ to choose what print and str() show:
def __str__(self):
return f"Sensor({self.name}, {len(self.readings)} readings)"
Methods with double underscores are called special methods (or "dunder" methods). Python calls them for you in particular situations: __init__ on creation, __str__ when printing, __len__ for len(obj), and so on.
Validating data
A class is a good place to enforce rules, so bad data is rejected at the door:
class BankAccount:
def __init__(self, owner, balance=0):
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("Insufficient funds")
self.balance -= amount
acct = BankAccount("Ada", 100)
acct.deposit(50)
acct.withdraw(30)
print(acct.balance) # 120
acct.withdraw(500) # ValueError: Insufficient funds
raise signals an error deliberately. The calling code can catch it with try / except ValueError (see the debugging lesson) or let the program stop with a clear message.
When to write a class
Classes are a tool, not a requirement. Plain functions and dictionaries are fine for small scripts. Consider a class when:
- several functions all take the same group of data as their first argument;
- you have many similar things (sensors, accounts, students), each with its own state;
- you want to enforce rules about how that data may change.
There is more to object-oriented programming, such as inheritance (a class that extends another) and the @dataclass shortcut from the dataclasses module that writes __init__ for you. Those build on exactly what you have seen here.
Common mistakes
- Forgetting
selfin the method definition.def add(value):fails withTypeError: add() takes 1 positional argument but 2 were given, because Python passes the object automatically. - Forgetting
self.when using an attribute. Inside a method,readingsis a local name; the attribute isself.readings. - Misspelling
__init__._init_or__int__will not be called when the object is created. - Creating mutable data at class level.
readings = []written directly underclass Sensor:(outside__init__) is shared by all sensors. Create it in__init__withself.readings = []. - Calling the method without parentheses.
tank.averageis the method itself;tank.average()runs it.
Quick recap
class Name:defines a class; callingName(...)creates an instance.__init__(self, ...)sets each new object's starting attributes.- Methods take
selffirst and useself.attributeto reach the object's data. __str__controls whatprint(obj)shows.- Use classes to bundle related data with the behaviour that belongs to it.
Try it
In the Do tab you will finish a Sensor class: store readings, report the latest and the average, and print a readable summary.