Section 1 of 12

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

Which line correctly defines a class TrialPatient that inherits from an existing Patient class?

Pre-test

A child class's __init__ needs to set the parent's attributes too. What is the recommended way?

Pre-test

TrialPatient defines a label method that overrides Patient.label. From inside TrialPatient.label, how do you call the parent's version?

Pre-test

TrialPatient inherits from Patient, which defines bmi(); TrialPatient does not define bmi() itself. What happens when you call tp.bmi() on a TrialPatient?

Pre-test

Why is super().__init__(...) preferred over copying the parent's self.x = x lines into the child?

Pre-test

Patient defines label(); TrialPatient overrides label() with its own version. For tp = TrialPatient(...), which version runs when you call tp.label()?

Pre-test

is_treatment() is defined only on the child TrialPatient, not on Patient. What happens if you call it on a plain Patient object?

Pre-test

Patient.label() returns the text Patient T001. A child overrides it as return "Trial " + super().label(). What does the child's label() return?

Pre-confidence

I can build a child class that inherits from a parent class, call super().__init__ correctly, and override one of the parent's methods.

Not at all confident
Fully confident
Section 2 of 12

2 Introduction

In Object-Oriented Programming, you write your first classes — bundling each patient's id, age, weight and height into a single Patient object with its own bmi() method. That works well until two classes start to look almost the same.

Imagine a TrialPatient: the same four fields as Patient, plus a trial_id and a trial_arm. Copying the Patient code and pasting in two extra attributes is tempting, but it leaves you with two near-identical classes to keep in sync. There is a cleaner way.

This module covers the everyday bits of object-oriented programming you will need:

  • Inheritance and extending classes — building a more specific class on top of a general one without copy-pasting code.
Inheritance lets one class reuse and build on the attributes and methods of another, instead of redefining them from scratch.
Inheritance lets one class reuse and build on the attributes and methods of another, instead of redefining them from scratch.

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 12

3 Inheritance and extending classes

Your Patient class works. Then a new requirement arrives: some patients are in a clinical trial, so on top of the basic four fields they also have a trial_id and a cohort label.

The tempting move is to copy the whole Patient class, rename it TrialPatient, and add the new attributes. It works, but now you have two copies of the same code. Fix a bug in Patient and you have to remember to fix it in TrialPatient too. Add a new method to Patient and the copy silently falls behind.

Inheritance is Python's way of saying: this new class is just like that one, plus a few extras.

We can also extend, add, new or override new methods in the class.

Subclasses can add new methods or override existing ones.
Subclasses can add new methods or override existing ones.
Section 3.1 of 12

3.1 Terminologies

Let’s first understand the terminologies.

  • Parent class (or base class) – The original class. Patient is our example
  • Child class (or sub class) – The new class that inherits from it e.g. TrialPatient
  • Super() – A way for the child to call methods on the parent
Section 3.2 of 12

3.2 Your first child class

The syntax for inheritance is one line: put the parent’s name in round brackets after the child’s name.

class TrialPatient(Patient):
 ...

You declare an inheriting class by putting the parent's name in round brackets after the class name: class TrialPatient(Patient):. From that single line, the new class automatically has every attribute and method the parent class has. You only need to write the extras.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
class Patient:
    def __init__(self, patient_id, age, weight_kg, height_m):
        self.patient_id = patient_id
        self.age = age
        self.weight_kg = weight_kg
        self.height_m = height_m

    def bmi(self):
        return self.weight_kg / (self.height_m ** 2)

class TrialPatient(Patient):
    def __init__(self, patient_id, age, weight_kg, height_m, trial_id, cohort):
        super().__init__(patient_id, age, weight_kg, height_m)
        self.trial_id = trial_id
        self.cohort = cohort

tp = TrialPatient("T001", 49, 80, 1.78, "TRIAL_A", "treatment")
print(tp.trial_id)
print(tp.bmi())
A subclass automatically receives every attribute and method from its parent class and can then add its own, so you reuse the parent's code instead of rewriting it.
A subclass automatically receives every attribute and method from its parent class and can then add its own, so you reuse the parent's code instead of rewriting it.

Two things to notice:

  • TrialPatient.__init__ takes all six parameters: the four shared ones plus the two new ones.
  • The first line of the child's __init__ is super().__init__(...), which hands the four shared values to the parent. The parent does its normal job of storing them on the instance. The child then adds its own extras on top.

Now to use it, you can just do the following:

tp = TrialPatient("T001", 49, 80, 1.78, "TRIAL_A", "treatment")

print(tp.trial_id) # "TRIAL_A" the new attribute we added
print(tp.cohort) # "treatment" also new
print(tp.age) # 49 inherited from Patient via super().__init__
print(tp.bmi()) # 25.25... inherited method, never rewritten

bmi was never written inside TrialPatient. It came along for free. That is the whole point of inheritance.

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__(self, symbol, chromosome):
  • g = Gene("APOE", 19)
  • g = Gene.new("APOE", 19)
  • def __init__(symbol, chromosome):
  • symbol = symbol
  • self.symbol = symbol
  • self.chromosome = chromosome
  • print(g.symbol)
  • class Gene:
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 3.3 of 12

3.3 Why super().__init__ is better

Let’s try and imagine this.

You have two classes:

class Patient:
    def __init__(self, patient_id, name, age, condition):
        self.patient_id = patient_id
        self.name = name
        self.age = age
        self.condition = condition

class TrialPatient(Patient):
    def __init__(self, patient_id, name, age, condition, trial_arm):
        super().__init__(patient_id, name, age, condition)
        self.trial_arm = trial_arm

TrialPatient is a Patient plus one extra thing (trial_arm). So it needs all four attributes from Patient, plus trial_arm.

You have two ways to give it those four attributes:

Option A: copy the four lines into TrialPatient.__init__ yourself.

class TrialPatient(Patient):
    def __init__(self, patient_id, name, age, condition, trial_arm):
        self.patient_id = patient_id
        self.name = name
        self.age = age
        self.condition = condition
        self.trial_arm = trial_arm

Option B: ask the parent to do it for you with super().__init__(...) as the code above.

Both work today. The reason Option B is preferred is maintenance. Imagine six months from now you decide that patient IDs should always be cleaned up before being stored, so you change Patient:

self.patient_id = patient_id.strip().upper()

If you used Option A, TrialPatient still has the old self.patient_id = patient_id line. The parent got smarter, the child silently didn't. Now your trial patients store messy IDs and your regular patients store clean ones. Bug.

With Option B, you didn't copy anything. You just said "parent, do your setup". Whatever the parent does now or later, the child inherits it automatically.

When the parent class is later updated, only the subclass that calls super().__init__() inherits the change. The subclass that copied the parent's setup keeps the old code and silently produces stale, buggy output.
When the parent class is later updated, only the subclass that calls super().__init__() inherits the change. The subclass that copied the parent's setup keeps the old code and silently produces stale, buggy output.

Let’s practise using super()

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
class Patient:
    def __init__(self, patient_id, weight_kg, height_m):
        self.patient_id = patient_id
        self.weight_kg = weight_kg
        self.height_m = height_m

    def label(self):
        return f"Patient {self.patient_id}"

class TrialPatient(Patient):
    def __init__(self, patient_id, weight_kg, height_m, trial_id):
        super().__init__(patient_id, weight_kg, height_m)
        self.trial_id = trial_id

    def label(self):
        return f"Trial patient {self.patient_id} ({self.trial_id})"

p = Patient("P001", 72, 1.75)
tp = TrialPatient("T001", 80, 1.78, "TRIAL_A")
print(p.label())
print(tp.label())
Section 3.4 of 12

3.4 Adding: Introducing new methods

The child class can define methods the parent does not have at all. These are just like any other method: indented inside the class, first parameter self.

class TrialPatient(Patient):
    def __init__(self, patient_id, age, weight_kg, height_m, trial_id, cohort):
        super().__init__(patient_id, age, weight_kg, height_m)
        self.trial_id = trial_id
        self.cohort = cohort

    def is_treatment(self): # brand new, not on Patient
        return self.cohort == "treatment"

    def summary(self): # also new, uses inherited bmi()
        return f"{self.patient_id} in {self.trial_id}: BMI {self.bmi():.1f}"

Two things worth noticing:

  • is_treatment uses self.cohort, which only exists on TrialPatient. A plainPatient does not have it, so this method would not make sense on the parent. That is exactly why it lives on the child.
  • summary calls self.bmi(), even though bmi is defined on Patient, not on TrialPatient. Inherited methods are reachable through self just like methods defined locally.
tp = TrialPatient("T001", 49, 80, 1.78, "TRIAL_A", "treatment")
print(tp.is_treatment()) # True
print(tp.summary()) # "T001 in TRIAL_A: BMI 25.2"

A plain Patient would raise AttributeError if you tried to call is_treatment() on it, which is correct: control patients and non-trial patients have no cohort, so the question does not apply to them.

A subclass inherits the parent's methods and introduces a new method that the parent does not have.
A subclass inherits the parent's methods and introduces a new method that the parent does not have.
Section 3.5 of 12

3.5 Overriding: replacing a parent’s method

Inheritance lets you add new methods but it also lets you replace existing ones. If the child defines a method with the same name as one of the parent, the child’s version wins.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
class Patient:
    def __init__(self, patient_id):
        self.patient_id = patient_id

    def label(self):
        return f"Patient {self.patient_id}"


class TrialPatient(Patient):
    def __init__(self, patient_id, trial_id):
        super().__init__(patient_id)
        self.trial_id = trial_id

    def label(self): # same name as parent's method, so this replaces it
        return f"Trial patient {self.patient_id} ({self.trial_id})"


p = Patient("P001")
tp = TrialPatient("T001", "TRIAL_A")

print(p.label()) # "Patient P001" parent's version
print(tp.label()) # "Trial patient T001 (TRIAL_A)" child's version

Python looks at the actual object first. For p, it finds label on Patient and uses it. For tp, it finds label on TrialPatient and uses that instead.

A subclass redefines a parent method under the same name, so subclass instances run the new version while the parent class is unchanged.
A subclass redefines a parent method under the same name, so subclass instances run the new version while the parent class is unchanged.
Section 3.6 of 12

3.6 Extending: calling the parent’s method as part of yours

Sometimes you do not want to replace the parent's method entirely, just wrap it. You can call the parent's version from inside the child's with super().method_name().

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
class TrialPatient(Patient):
    def __init__(self, patient_id, trial_id):
        super().__init__(patient_id)
        self.trial_id = trial_id

    def label(self):
        return f"[{self.trial_id}] " + super().label() # prepend, then reuse parent's work


tp = TrialPatient("T001", "TRIAL_A")
print(tp.label()) # "[TRIAL_A] Patient T001"

The child gets the prefix it wanted, and Patient.label still owns the formatting of the base part. If Patient.label ever changes, the child automatically picks up the change.

A subclass redefines a parent method but calls super() to reuse the parent's behaviour, then appends its own.
A subclass redefines a parent method but calls super() to reuse the parent's behaviour, then appends its own.
Section 4 of 12

4 Putting it together

In real code these three ideas show up together. You define a base class for the kind of object you are working with, you build one or two more specific subclasses on top of it, and you give some of the transformation methods a return self so they chain. The worked example below ties all three ideas into one short routine: a Sample class with two transformation methods, a BloodSample subclass that adds a haemoglobin attribute and overrides the label, and a final chained call that cleans up an instance and prints it.

Let’s practise with the code arrangement first.

Parsons problem · Build a small class hierarchy and chain a method

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: Define a Patient class with patient_id and age, and a Counter-style method add_year that increases age by 1 and returns self. Then create a single Patient with id "P001" and age 54, chain add_year onto it twice, and print the resulting age.

Line bank
  • p = Patient.new("P001", 54)
  • def __init__(self, patient_id, age):
  • p.add_year().add_year()
  • p = Patient("P001", 54)
  • class Patient:
  • def Patient(patient_id, age):
  • p = p.add_year().add_year()
  • self.age = age
  • return self
  • self.age = self.age + 1
  • def add_year(self):
  • print(p.age)
  • self.patient_id = patient_id
  • self.age += 1
Your solution
  • Drop lines here, in order.

and now a worked example!

Worked example · Define a class, extend it, and chain its methods

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: Build a Sample base class with a patient_id, a weight, and two chainable cleaning methods. Then extend it into a BloodSample child class that adds a haemoglobin level and a custom label. Finally, create one BloodSample, run a chain of cleaning steps on it, and print its label.

Stage 1 · Study the solved example
Fully solved solution
class Sample:
    def __init__(self, patient_id, weight):
        self.patient_id = patient_id
        self.weight = weight

    def round_weight(self):
        self.weight = round(self.weight, 1)
        return self

    def label(self):
        return f"Sample {self.patient_id}"

class BloodSample(Sample):
    def __init__(self, patient_id, weight, haemoglobin):
        super().__init__(patient_id, weight)
        self.haemoglobin = haemoglobin

    def label(self):
        return f"Blood {self.patient_id} (Hb {self.haemoglobin})"

b = BloodSample("P001", 72.456, 13.4)
print(b.round_weight().label())
Walk-through
  1. The Sample class holds the shared data (patient_id, weight) and the shared method round_weight, which ends with return self so it can be chained.
  2. BloodSample inherits from Sample, calls super().__init__ to set the shared fields, and then adds its own haemoglobin attribute. It also overrides label so a blood sample reports itself differently.
  3. The final line creates one BloodSample, chains round_weight onto it (which mutates the weight and returns the same object), and then calls label on that returned object — all in one expression.
Section 5 of 12

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

Which line correctly defines a class TrialPatient that inherits from an existing Patient class?

Post-test

A child class's __init__ needs to set the parent's attributes too. What is the recommended way?

Post-test

TrialPatient defines a label method that overrides Patient.label. From inside TrialPatient.label, how do you call the parent's version?

Post-test

TrialPatient inherits from Patient, which defines bmi(); TrialPatient does not define bmi() itself. What happens when you call tp.bmi() on a TrialPatient?

Post-test

Why is super().__init__(...) preferred over copying the parent's self.x = x lines into the child?

Post-test

Patient defines label(); TrialPatient overrides label() with its own version. For tp = TrialPatient(...), which version runs when you call tp.label()?

Post-test

is_treatment() is defined only on the child TrialPatient, not on Patient. What happens if you call it on a plain Patient object?

Post-test

Patient.label() returns the text Patient T001. A child overrides it as return "Trial " + super().label(). What does the child's label() return?

Post-confidence

I can build a child class that inherits from a parent class, call super().__init__ correctly, and override one of the parent's methods.

Not at all confident
Fully confident
Section 6 of 12

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)