Section 1 of 8

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.

Pre-test

Inside a class, what is the role of the self parameter in a method?

Pre-test

For methods to be usable in a chain like obj.step1().step2(), what must each method do?

Pre-test

When does a class's __init__ method run?

Pre-test

After p1 = Patient("P001", 54), which word best describes p1?

Pre-test

An object p1 was built from a Patient class. How do you read its age attribute?

Pre-test

The method def bmi(self): takes no other parameters. How does it get the weight and height it needs?

Pre-test

A method changes the object but ends without return self. What happens when you run obj.add(3).add(5)?

Pre-test

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?

Pre-confidence

I can define a class with an __init__ method that stores attributes on the instance, and create new objects from that class.

Not at all confident
Fully confident
Pre-confidence

I can write a method that returns self so that several calls on the same object can be chained together in one expression.

Not at all confident
Fully confident
Section 2 of 8

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 class keyword, the __init__ method, the self parameter, 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.

Section 3 of 8

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.62

And 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 bmi function 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.

Classes bundle related variables and behaviours into one named object for consistency.
Classes bundle related variables and behaviours into one named object for consistency.
Section 3.1 of 8

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.age is 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.
  • self is how a method refers to the specific object it is running on. When you call p1.bmi(), Python passes p1 in as self automatically
Important terminologies in object-oriented programming.
Important terminologies in object-oriented programming.

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.

__init__ stores all arguments into attributes.
__init__ stores all arguments into attributes.

Ready? Let’s go create our first class!

Section 3.2 of 8

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)
Different components that make up a class.
Different components that make up a class.

Three things to notice:

  • The class name is PascalCase by convention (Patient, not patient or patient_class).
  • __init__ runs once per object, at creation time. It takes self first, then whatever you want to pass in.
  • bmi is a method, so it also takes self first. It does not need weight_kg or height_m as parameters because it can read them off self.

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.

Parsons problem · Build a Gene class

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.

Line bank
  • 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 = chromosome
  • symbol = symbol
  • self.symbol = symbol
Your solution
  • Drop lines here, in order.

Now, let’s try it out!

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
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())
Section 4 of 8

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.

Method chaining runs several operations on one object in a single line by passing it from one method to the next.
Method chaining runs several operations on one object in a single line by passing it from one method to the next.

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.

Method chaining works because each method returns the object it just modified via return self, so the next call has the same object to act on; without that return, the first method gives back None and the chain breaks at the very next call.
Method chaining works because each method returns the object it just modified via return self, so the next call has the same object to act on; without that return, the first method gives back None and the chain breaks at the very next call.

Try it out yourself!

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
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!

Parsons problem · Chainable SymptomList

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.

Line bank
  • self.items = []
  • return
  • s = SymptomList.add("Fever").add("Cough")
  • print(s.items)
  • def __init__(self):
  • class SymptomList:
  • s = SymptomList().add("Fever").add("Cough")
  • def add(self, name):
  • return self
  • self.items.append(name.lower())
  • return self.items
Your solution
  • Drop lines here, in order.

Now, let’s build a chainable PatientRecord!

Worked example · Building 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.

Stage 1 · Study the solved example
Fully solved solution
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)
Walk-through
  1. We define the class and initialise id and symptoms to empty values inside __init__.
  2. 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.
  3. We instantiate with PatientRecord() and immediately chain .set_id(...) and two .add_symptom(...) calls before the chain terminates and we read the attributes.
Section 5 of 8

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.

Post-test

Inside a class, what is the role of the self parameter in a method?

Post-test

For methods to be usable in a chain like obj.step1().step2(), what must each method do?

Post-test

When does a class's __init__ method run?

Post-test

After p1 = Patient("P001", 54), which word best describes p1?

Post-test

An object p1 was built from a Patient class. How do you read its age attribute?

Post-test

The method def bmi(self): takes no other parameters. How does it get the weight and height it needs?

Post-test

A method changes the object but ends without return self. What happens when you run obj.add(3).add(5)?

Post-test

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?

Post-confidence

I can define a class with an __init__ method that stores attributes on the instance, and create new objects from that class.

Not at all confident
Fully confident
Post-confidence

I can write a method that returns self so that several calls on the same object can be chained together in one expression.

Not at all confident
Fully confident
Section 6 of 8

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.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)