1 Before you start
Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.
Inside a class, what is the role of the self parameter in a method?
For methods to be usable in a chain like obj.step1().step2(), what must each method do?
When does a class's __init__ method run?
After p1 = Patient("P001", 54), which word best describes p1?
An object p1 was built from a Patient class. How do you read its age attribute?
The method def bmi(self): takes no other parameters. How does it get the weight and height it needs?
A method changes the object but ends without return self. What happens when you run obj.add(3).add(5)?
Counter starts self.n at 0; its add(x) runs self.n = self.n + x then return self. What is Counter().add(3).add(5).add(2).n?
I can define a class with an __init__ method that stores attributes on the instance, and create new objects from that class.
I can write a method that returns self so that several calls on the same object can be chained together in one expression.
2 Introduction
Once your scripts grow beyond a handful of variables, things get unwieldy. Reading in a patient's id, age, weight, and height as four separate names works at first, but you soon find yourself passing the same cluster of values into every function, in the same order, every time. Scale that up to a list of patients and the approach starts to break down.
A class solves this by bundling related data and behaviour under one name. Instead of juggling loose variables alongside functions that all operate on them, you create a single object that holds its own values and knows what to do with them.
This module covers the everyday bits of object-oriented programming you will need:
- Defining classes and creating instances — the
classkeyword, the__init__method, theselfparameter, attributes, methods, and the dot syntax for using them. - Method chaining — writing methods that return the object itself so you can string several calls together in a single fluent line.
Try every snippet in the Python Scratchpad on the right. By the end of the module you will define a Patient class, extend it into a TrialPatient class, and chain a small pipeline of cleaning steps on a Sample object.
3 Classes and why it matters
Picture the situation that pushes most people into using classes.
You are tracking patients. For each one you have an id, an age, a weight in kilograms, and a height in metres. You start the obvious way:
pid, age, weight_kg, height_m = "P001", 54, 82.0, 1.75
def bmi(weight_kg, height_m):
return weight_kg / (height_m ** 2)
print(bmi(weight_kg, height_m))Now you get a second patient.
pid2, age2, weight_kg2, height_m2 = "P002", 41, 68.5, 1.62And a third. By patient ten, you have forty variables on the screen, no way to loop, and a guarantee that something will get messed up!
Now, the usual fix is a list of dictionaries:
patients = [
{"pid": "P001", "age": 54, "weight_kg": 82.0, "height_m": 1.75},
{"pid": "P002", "age": 41, "weight_kg": 68.5, "height_m": 1.62},
]
for p in patients:
print(p["pid"], bmi(p["weight_kg"], p["height_m"]))This can work, but two annoyances stand out:
- Every field access is
p["<field">]with brackets and quotes, for example,p["weight_kg"]. This impedes readability. - The
bmifunction lives off to the side as a separate function. It has no built-in connection to a patient, so you must remember to pass the right keys in the right order every time.
A class fixes both.

3.1 Terminologies
Before that, let’s lock down some of these terminologies,
- Class – A blueprint. It describes what data a thing holds and what it can do e.g. the class
Patient(…) - Object (or instance) – A specific thing built from the blueprint. For example,
p1 = Patient(…)creates one object - Attribute – A piece of data stored on an object. For example,
p1.ageis an attribute - Method – A function that belongs to a class and operates on the object. For example,
p1.bmi()is a method
Two more pieces of syntax you will see immediately:
__init__is the constructor. Python calls it automatically the moment you create a new object. Note that you are using a double underscore.selfis how a method refers to the specific object it is running on. When you callp1.bmi(), Python passesp1in asselfautomatically

The __init__ runs once when you create an object, takes the arguments you pass in, and saves them onto self so every method in the class can use them later.

Ready? Let’s go create our first class!
3.2 Defining classes and creating instances
Before we start, here’s the anatomy of a class
class Patient:
def __init__(self, pid, age, weight_kg, height_m):
self.pid = pid # store the id on this object
self.age = age # store the age on this object
self.weight_kg = weight_kg # store the weight on this object
self.height_m = height_m # store the height on this object
def bmi(self):
return self.weight_kg / (self.height_m ** 2)
Three things to notice:
- The class name is PascalCase by convention (
Patient, notpatientorpatient_class). __init__runs once per object, at creation time. It takesselffirst, then whatever you want to pass in.bmiis a method, so it also takesselffirst. It does not needweight_kgorheight_mas parameters because it can read them offself.
Now that you have initialized the class, you can use it as such:
p1 = Patient("P001", 54, 82.0, 1.75)
p2 = Patient("P002", 41, 68.5, 1.62)
print(p1.pid)
print(p1.bmi())
print(p2.bmi())Each patient is its own object with its own values, but they all share the structure and behaviour defined by the Patient class.
Now your turn to understand the order of the syntax.
All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.
Task: Build a class Gene that stores a gene symbol and a chromosome number, then create one and print its symbol.
def __init__(symbol, chromosome):class Gene:g = Gene.new("APOE", 19)def __init__(self, symbol, chromosome):print(g.symbol)g = Gene("APOE", 19)self.chromosome = chromosomesymbol = symbolself.symbol = symbol
- Drop lines here, in order.
Now, let’s try it out!
Try this snippet in the Python Scratchpad on the right.
class BloodPressure:
def __init__(self, systolic, diastolic):
self.systolic = systolic
self.diastolic = diastolic
def category(self):
if self.systolic >= 140 or self.diastolic >= 90:
return "hypertensive"
if self.systolic >= 130 or self.diastolic >= 80:
return "elevated"
return "normal"
readings = [BloodPressure(118, 76), BloodPressure(134, 82), BloodPressure(152, 94)]
for bp in readings:
print(bp.systolic, bp.diastolic, bp.category())
4 Method chaining
Method chaining is a way of writing several operations on the same object as one continuous line, by having each method hand the object back to the next one. This matters because real data work rarely involves one clean step: when you need to standardise units, strip whitespace, fill missing values, and flag quality issues on the same sample, chaining lets you express the whole pipeline as one readable line instead of four near-identical ones.

You have already used method chaining without anyone calling it that. Take a messy patient ID and clean it up:
raw = " P001 "
raw.strip().lower().replace("p", "P")Three methods, one line, no intermediate variables. This works because each string method returns a new string, so the next dot has something to call on. That is the whole idea behind method chaining: each call hands an object to the next call.
You can write your own classes that behave this way. The trick is that each method must return the object it just modified, using return self. Here is the simplest possible example, a counter you can keep adding to:
class Counter:
def __init__(self):
self.n = 0
def add(self, x):
self.n = self.n + x
return self
c = Counter().add(3).add(5).add(2)
print(c.n)Without return self, add(3) would return None, and None.add(5) is meaningless.

Try it out yourself!
Try this snippet in the Python Scratchpad on the right.
class SymptomList:
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
return self
def strip_spaces(self):
self.items = [s.strip() for s in self.items]
return self
def lowercase(self):
self.items = [s.lower() for s in self.items]
return self
def dedupe(self):
self.items = list(dict.fromkeys(self.items))
return self
s = SymptomList().add(" Fever ").add("Cough").add("FEVER").strip_spaces().lowercase().dedupe()
print(s.items)
Two rules of thumb for designing chainable methods:
- If a method transforms the object, end with
return self. - Each step should read as a verb acting on the same object. For example,
standardise_units,strip_strings,fill_missing. Chains of unrelated calls glued together with dots are harder to follow, not easier.
When to know use or not use method chaining?
A single step reads fine on its own line.
Method chaining becomes important when you need to transform multiple methods into one larger operation.
Let’s practice doing method chaining. First, let’s try and get the syntax arrangement correct!
All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.
Task: Assemble a class whose .add() method returns self so the calls can be chained. Each .add() should lowercase the symptom before appending.
self.items = []returns = SymptomList.add("Fever").add("Cough")print(s.items)def __init__(self):class SymptomList:s = SymptomList().add("Fever").add("Cough")def add(self, name):return selfself.items.append(name.lower())return self.items
- Drop lines here, in order.
Now, let’s build a chainable PatientRecord!
Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.
Problem: Write a PatientRecord class with two chainable methods, .set_id(id_str) and .add_symptom(name). Build a record for patient "P001" with symptoms fever and cough, then print the id and symptoms.
class PatientRecord:
def __init__(self):
self.id = None
self.symptoms = []
def set_id(self, id_str):
self.id = id_str
return self
def add_symptom(self, name):
self.symptoms.append(name)
return self
p = PatientRecord().set_id("P001").add_symptom("fever").add_symptom("cough")
print(p.id, p.symptoms)
- We define the class and initialise id and symptoms to empty values inside __init__.
- Each chainable method updates the attribute it owns and ends with return self — that is the line that hands the same object back to the next dot in the chain.
- We instantiate with PatientRecord() and immediately chain .set_id(...) and two .add_symptom(...) calls before the chain terminates and we read the attributes.
The same solution with key parts replaced by ???.
Fill in every ??? so the code matches the reference,
then ask the tutor to check it.
Your turn: Write a chainable DrugRegimen class with two methods: .set_patient(pid) and .add_drug(name). Build a regimen for patient "P042" with drugs "metformin" and "lisinopril". Print the patient id followed by the drug list.
5 Check your understanding
You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.
Inside a class, what is the role of the self parameter in a method?
For methods to be usable in a chain like obj.step1().step2(), what must each method do?
When does a class's __init__ method run?
After p1 = Patient("P001", 54), which word best describes p1?
An object p1 was built from a Patient class. How do you read its age attribute?
The method def bmi(self): takes no other parameters. How does it get the weight and height it needs?
A method changes the object but ends without return self. What happens when you run obj.add(3).add(5)?
Counter starts self.n at 0; its add(x) runs self.n = self.n + x then return self. What is Counter().add(3).add(5).add(2).n?
I can define a class with an __init__ method that stores attributes on the instance, and create new objects from that class.
I can write a method that returns self so that several calls on the same object can be chained together in one expression.
6 Your results
Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.
Submit the post-test to see your results.
What is the one thing from this module that is still unclear to you?