← All lessons  ·  Cambridge 9618 A Level · Unit 20.1 · Paper 4 Practical

Object-Oriented Programming
Home Lesson 4 — Containment or Inheritance?

Work through this page, then turn the club into a class that contains its members, and submit the Evidence Document on Teams.

Home lesson 4 of 6 Language: Python (console) Suggested time: 60 minutes Submit: Evidence Document (.docx)

Start here

How this lesson works

Same routine. This page teaches, then makes you type — and this week the club itself becomes a class that has its members inside it.

  1. Learn on this page. Predict, run, investigate, modify. Nothing to install.
  2. Open your Lesson 3 code. The Member class does not change. Last week the array of members and the functions that worked on it sat in the main program; this week they move inside a new Club class. If your Member class is not working, there is a version in the task section to start from.
  3. Evidence it. Paste your code as text and your screenshot into this week's Evidence Document, and submit it on Teams.

The trap this lesson is about

In five examiner reports since November 2022, across four exam series, Cambridge has named candidates using inheritance where the design called for containment — the equivalent of writing class Club(Member): when a club has an array of members. The code often runs. It still loses the class-declaration marks, and usually the constructor marks with them, because the candidate built the wrong relationship. The whole decision takes ten seconds once you know where to look in the design table — and that is what you are about to learn.


Retrieval

Do Now — three minutes, no notes

Answer these before you read anything else. Write them in your book, then reveal the answers and mark them.

Questions

  1. An array Team holds four Player objects, in Team[0] to Team[3]. Each has a get method GetName(). Write one line of Python that outputs the name of the last player. [1]
  2. A linear search has else: return -1 inside its loop. State what goes wrong when the item being searched for is not in the first element. [1]
  3. A class design lists the attribute Members : ARRAY[0:29] OF Member. State how many elements the array has, and the data type of each element. [2]
Mark scheme — mark your own answers
QAcceptedMarks
1print(Team[3].GetName())
Reject: Team[4] (past the end), Team.GetName(3), Team[3].__Name.
1
2The function returns -1 after checking only the first element / the loop never reaches the other elements, so the item is reported as not found.1
3MP1 — 30 elements (0 to 29).
MP2 — each element is a Member — an object of the class Member.
Reject for MP2: "ARRAY", "STRING", "a record".
2

Q3 is the prerequisite check. A data type can be another class. When you see a class name in the data type column of a design, that is the signal this whole lesson is built on. If you wrote "ARRAY" or "STRING", reread the row before you go on.


Where this is going

Learning objectives

By the end of this lesson I can…SyllabusPaper
Decide from a class design whether two classes are related by containment or inheritance, and justify it with has a or is a.20.13 & 4
Declare a class with a private attribute that is an array of objects of another class, with its type evidenced and initialised in the constructor.20.14
Write methods of the containing class that work through the contained objects' own methods.20.14
Write an add method that checks for space and returns a Boolean on every path.20.14

Vocabulary

Key terms — learn these wordings

TermDefinition that earns the mark
Containment
(aggregation)
A relationship where one class has an attribute (property) that is an object, or an array of objects, of another class. The container has the other objects.
InheritanceA relationship where a child class derives the attributes (properties) and methods of a parent class, and can add its own or override them. The child is a kind of the parent.
Parent class
(superclass, base class)
The class whose attributes and methods are inherited.
Child class
(subclass, derived class)
The class that inherits from a parent class.

How definitions lose the mark

Don't define a word with itself. Examiners reported (Nov 2022) that "a new class inherits the attributes and methods from a parent class" was not enough to define inheritance — they wanted it to derive them. Cambridge's standing instruction: use the words property, method, class and object when you define an OOP term.

"Containment is when a class is inside another class" is risky — it sounds as if one class is declared inside the other, which is not what happens. Say that one class has an attribute whose value is an object, or an array of objects, of the other class.


PRIMM · Step 1

Predict

Read this program carefully. Do not run it yet. There are two classes now. Write down exactly what you think the two print lines will output.

Python — Paper 4
class Book:
    def __init__(self, Title, Pages):
        self.__Title = Title        # STRING
        self.__Pages = Pages        # INTEGER

    def GetTitle(self):
        return self.__Title

    def GetPages(self):
        return self.__Pages

class Shelf:
    def __init__(self, Label):
        self.__Label = Label        # STRING
        self.__Books = [None] * 3   # ARRAY[0:2] OF Book
        self.__Count = 0            # INTEGER

    def AddBook(self, NewBook):
        self.__Books[self.__Count] = NewBook
        self.__Count = self.__Count + 1

    def TotalPages(self):
        Total = 0
        for Index in range(0, self.__Count):
            Total = Total + self.__Books[Index].GetPages()
        return Total

Fiction = Shelf("Fiction")
Fiction.AddBook(Book("Dune", 412))
Fiction.AddBook(Book("Emma", 474))
print(Fiction.TotalPages())
Fiction.AddBook(Book("Holes", 233))
print(Fiction.TotalPages())

Commit your prediction before you scroll

Two harder questions: is a Shelf a kind of Book? If not, what connects the two classes? And why does the loop run to self.__Count rather than to 3?


PRIMM · Step 2

Run

Now run it and compare the real output with your prediction.

Python 3 — try it Python is loading…
Output
Press Run to execute.

Your code and your prediction are saved in this browser, so closing the tab does not lose them. If a program never finishes, press Stop.

Check your prediction
886
1119
  • The first total is taken when the shelf holds two books (412 + 474). The second is taken after a third is added. The shelf remembers its books between calls because they are stored in its own attribute.
  • A Shelf is not a kind of Book. A shelf has books: its attribute __Books is an array whose elements are Book objects. That relationship is called containment.
  • The loop runs to self.__Count because that is how many books are actually stored. The array has three elements, but until a book is added an element holds None — there is no book there to ask.

Try this — then read why it matters

In TotalPages, change range(0, self.__Count) to range(0, 3) and run it again.

It crashes: 'NoneType' object has no attribute 'GetPages'. On the first total only two books are stored, so element 2 is still None, and None has no methods. A container loops over the objects it has, not over the size of its array. That is what the counter is for. Change it back before you go on.


PRIMM · Step 3

Investigate — has a, or is a?

1. The one-question test

Say the two class names in a sentence. If "A ___ is a ___" is true, it is inheritance. If "A ___ has ___" is true, it is containment. Only one of them will sound right.

SentenceTrue?Relationship
A shelf is a book.✗ NoContainment — Shelf has an array of Book
A shelf has books.✓ Yes
A novel is a book.✓ YesInheritance — Novel is a child of Book
A club has members.✓ YesContainment
A savings account is an account.✓ YesInheritance

2. Where the design table tells you

In the exam you do not have to guess from the names. The design says which it is — you just have to look in the right place.

Containment — look for…Inheritance — look for…
Another class's name in the data type column of an attribute:
Books : ARRAY[0:2] OF Book
Captain : Member
Words in the description such as "is a subclass of", "inherits from", "child class of" — or a class diagram with an arrow from the child to the parent.

The rule to apply: if the other class appears as an attribute, it is containment — no brackets after the class name and no super().

Checkpoint 1

A class design reads: Class Garage. Attributes: GarageName : STRING; Vehicles : ARRAY[0:9] OF Car. Which class header follows the design?

3. What containment looks like in Python

class Shelf:                            # 1. no parent class in brackets
    def __init__(self, Label):
        self.__Label = Label            # STRING
        self.__Books = [None] * 3       # 2. ARRAY[0:2] OF Book, private
        self.__Count = 0                # 3. INTEGER — how many are stored
  1. No parent class. class Shelf: — nothing in brackets, and so no super().__init__ anywhere.
  2. The contained objects live in a private attribute, created in the constructor, with its type shown in a comment exactly as you did for STRING and INTEGER. It is not a constructor parameter unless the design says so.
  3. A counter records how many objects are actually stored, so that methods can loop over those and no further.

Read what "empty" means in the design

Here the elements start empty — the comment says so — so [None] * 3 with a type comment is right — examiners describe stronger Python candidates as having "used a comment to indicate the data type intended" (Nov 2021). But some designs say the array is initialised with objects — for example twenty nodes each holding -1. Then an object must be created and stored in every element; None will not earn the mark (Jun 2024). The design decides, not habit.

4. Work through their methods, not their attributes

Inside Shelf, a book's title is still private to Book. The shelf has to ask for it, exactly as the main program did last week.

Inside a Shelf method you writeWhat happens
self.__Books[Index].GetPages()✓ The book returns its pages.
self.__Books[Index].__Pages✗ AttributeError: 'Book' object has no attribute '_Shelf__Pages'. Private means private to the class that declared it. Shelf is a different class.
self.__Books.GetPages(Index)✗ Crash — the array is a list, and a list has no GetPages. Pick the element first.

Pass the object, not its data

Here AddBook(NewBook) takes one parameter, and it is a Book — in the exam, the class design tells you this. So the caller creates the object and hands it over: Fiction.AddBook(Book("Dune", 412)). Writing AddBook(self, Title, Pages) and building the book inside does not follow the design. Examiners have reported exactly this — passing the integer and string values instead of the object — in Nov 2022 and again in Jun 2024.

Checkpoint 2

Inside a method of the class Team, the attribute self.__Players holds Player objects, and each Player has a get method GetGoals(). Which line, inside for Index in range(0, self.__Count):, adds one player's goals to Total?

5. For recognition only — what inheritance looks like

Next week is all about inheritance. For now you only need to recognise it, so that you can tell the two apart.

class Novel(Book):                       # a Novel IS A Book
    def __init__(self, Title, Pages, Genre):
        super().__init__(Title, Pages)   # the parent's constructor sets these
        self.__Genre = Genre             # STRING — the one extra attribute
ContainmentInheritance
SentenceA shelf has booksA novel is a book
Class headerclass Shelf:class Novel(Book):
Other class appears…as an attribute's data typein brackets as the parent
super().__init__✗ never✓ always, in the constructor
Cambridge pseudocode (Paper 3)CLASS Shelf
PRIVATE Books : ARRAY[0:2] OF Book
CLASS Novel INHERITS Book
SUPER.NEW(Title, Pages)

6. Design it yourself

Paper 3 and Paper 4 both expect you to design classes from a description, not only to code a design you are given. Read this scenario and write down the classes, which ones are related by containment and which by inheritance, and each class's attributes with data types.

Scenario — a cinema

A cinema has a name and five screens. Each screen has a screen number and a number of seats. A 3D screen is a screen that also records how many pairs of 3D glasses it has available.

Your design — commit it before you open the answer

A design that would score
ClassAttributesRelationship
CinemaCinemaName : STRING
Screens : ARRAY[0:4] OF Screen
Containment — a cinema has screens, so Screen appears as a data type.
ScreenScreenNumber : INTEGER
Seats : INTEGER
The parent class.
Screen3DGlassesAvailable : INTEGERInheritance — a 3D screen is a screen. It derives ScreenNumber and Seats, and adds only its own attribute.

Two traps: giving Screen3D its own copies of ScreenNumber and Seats (inheritance already provides them), and making Cinema inherit from Screen because the description mentions both in one sentence.


PRIMM · Step 4

Modify

First, add a fourth book to the end of the program — Fiction.AddBook(Book("Beloved", 324)) — and run it. It crashes with IndexError: list assignment index out of range: the shelf has only three elements, and AddBook never checks. Now fix it.

Changes to make

  1. Change AddBook so that it only stores the book when the shelf has space. It returns True if the book was added and False if the shelf was full.
  2. Replace the main program. Add these four books, in order: Dune (412), Emma (474), Holes (233), Beloved (324).
  3. Store what each call to AddBook returns. When it is False, output the book's title followed by did not fit.
  4. Finally, output Total pages: 1119 using TotalPages().
Model answer — check yours against this
Python — Paper 4
    def AddBook(self, NewBook):
        if self.__Count < 3:
            self.__Books[self.__Count] = NewBook
            self.__Count = self.__Count + 1
            return True
        else:
            return False

Fiction = Shelf("Fiction")
for NewBook in [Book("Dune", 412), Book("Emma", 474), Book("Holes", 233), Book("Beloved", 324)]:
    Added = Fiction.AddBook(NewBook)
    if Added == False:
        print(NewBook.GetTitle() + " did not fit")
print("Total pages: " + str(Fiction.TotalPages()))

Expected output:

Beloved did not fit
Total pages: 1119
  • Check, then store, then count. The book goes into element self.__Count — the next free one — and only then does the counter go up. Doing it the other way round leaves element 0 empty and runs off the end one book early.
  • A Boolean on every path. Both branches return something. A method that returns True when it works and nothing at all when it does not hands the caller None.
  • Four separate lines of Added = Fiction.AddBook(...) and a test after each is equally good. The loop over a list of new books is just shorter.

The marks

Exam technique — the data type column decides

A music app stores playlists. The design says:

Class design — Playlist

AttributeData typeSet when the object is created
PlaylistNameSTRINGFrom a parameter
SongsARRAY[0:49] OF SongFifty empty elements
SongCountINTEGERAlways 0

Predict, then reveal. A candidate writes this constructor. It runs without an error. Decide what an examiner would credit before you open the answer.

Candidate response
class Playlist(Song):
    def __init__(self, PlaylistName, Title, Artist):
        super().__init__(Title, Artist)
        self.__PlaylistName = PlaylistName    # STRING
        self.__SongCount = 0                  # INTEGER
What the examiner says

It runs. It still loses most of the declaration marks.

  • ✗ Inherits from Song. A playlist is not a song. Song is in the data type column of an attribute, so this is containment. The class-header mark is lost.
  • ✗ Wrong parameters. The design gives the constructor one parameter, PlaylistName. Title and Artist belong to a song, and are only here because the candidate chose inheritance.
  • ✗ No Songs array. The attribute that holds the songs is missing entirely. Every later part — adding a song, listing the songs, counting them — now has nowhere to work, so the error costs marks well beyond this part.
  • ✓ PlaylistName and SongCount are private, typed, and SongCount is set to the fixed value 0 inside the constructor. Credited.

What would have scored

class Playlist:
    def __init__(self, PlaylistName):
        self.__PlaylistName = PlaylistName    # STRING
        self.__Songs = [None] * 50            # ARRAY[0:49] OF Song
        self.__SongCount = 0                  # INTEGER

The habit to build: before you write the class header, run your finger down the data type column. If the name of another class is there, it is containment — no brackets after the class name, no super(), and that class's parameters stay out of your constructor.

Checkpoint 3

A design reads: "ElectricCar is a subclass of Car. It has one additional attribute, BatteryRange : INTEGER." Which of these follows the design?


Assessed work

Over to you — the club becomes a class

Everyone starts at Independent. Drop to Scaffolded if you stall. Go to Exam Conditions when you have finished.

Functions become methods

Last week TotalCredits and FindMember were functions in the main program. This week they belong to the club, so they are written inside class Club, take self, and are called on the object — TheClub.TotalCredits(). Examiners reported in Nov 2024 that "some candidates wrote a function and not a method".

The scenario

The sports club is now represented by a class. The class Member is unchanged from Lesson 2. A new class, Club, has the following design.

AttributeData typeSet when the object is created
ClubNameSTRINGFrom a parameter
MembersARRAY[0:4] OF MemberFive empty elements
NumberOfMembersINTEGERAlways 0
MethodDescription
ConstructorInitialises all three attributes as described above.
AddMember(NewMember)If the club has fewer than 5 members, stores NewMember in the next free element of Members, adds 1 to NumberOfMembers and returns TRUE. Otherwise returns FALSE.
TotalCredits()Returns the total credits of all the members stored.
DisplayMember(SearchName)Calls DisplayDetails() for the member whose name matches SearchName. If no stored member matches, outputs SearchName followed by " not found".

Test data — use it in this order:

MemberIDNameAgeCredits to add
"M4021""Sara Ahmed"1620
"M4022""Omar Khalid"1745
"M4023""Lina Haddad"1660
"M4024""Yusuf Rahman"180
"M4025""Mia Chen"1735
"M4026""Tom Reilly"1610
Member class not working? Start from this version

Copy this into your editor and build Club underneath it.

class Member:
    def __init__(self, MemberID, Name, Age):
        self.__MemberID = MemberID   # STRING
        self.__Name = Name           # STRING
        self.__Age = Age             # INTEGER
        self.__Credits = 0           # INTEGER
        self.__Active = True         # BOOLEAN

    def DisplayDetails(self):
        print("Member ID : " + self.__MemberID)
        print("Name      : " + self.__Name)
        print("Age       : " + str(self.__Age))
        print("Credits   : " + str(self.__Credits))
        print("Active    : " + str(self.__Active))

    def GetName(self):
        return self.__Name

    def GetCredits(self):
        return self.__Credits

    def SetAge(self, NewAge):
        self.__Age = NewAge

    def AddCredits(self, Amount):
        NewTotal = self.__Credits + Amount
        if NewTotal > 50:
            NewTotal = 50
        self.__Credits = NewTotal
Independent — start here

(a) Write program code to declare the class Club and its constructor. [4]

(b) Write program code for the method AddMember(). [3]

(c) Write program code for the method TotalCredits(). [3]

(d) Write program code for the method DisplayMember(). [2]

(e) Write a main program that:

  • creates a Club object with the name "Riverside Juniors"
  • for each row of test data, in order: creates a Member object, adds its credits using AddCredits(), then adds it to the club using AddMember() — storing the value returned and outputting the member's name followed by could not be added: club full when it is FALSE
  • outputs the total credits, using TotalCredits(), with the label Total credits:
  • calls DisplayMember("Lina Haddad") and then DisplayMember("Tom Reilly"). [2]

(f) Take a screenshot showing the program running. It must show the test data and the complete output. [2]

Scaffolded — only if you stall

Same questions, same marks. Put this below your Member class and complete every ______ and # ....

class Club:                                   # no brackets: a club HAS members
    def __init__(self, ______):
        self.__ClubName = ______                  # STRING
        self.__Members = [None] * ______          # ARRAY[0:4] OF Member
        # ... NumberOfMembers (fixed value — not a parameter)

    def AddMember(self, NewMember):
        if self.__NumberOfMembers < ______:
            self.__Members[______] = NewMember    # the next free element
            # ... add 1 to NumberOfMembers
            return ______
        else:
            return ______

    def TotalCredits(self):
        Total = 0
        for Index in range(0, ______):            # only the members stored
            Total = Total + self.__Members[Index].______()
        return Total

    def DisplayMember(self, SearchName):
        Found = False
        for Index in range(0, self.__NumberOfMembers):
            if self.__Members[Index].______() == SearchName:
                # ... call DisplayDetails on this member
                Found = True
        # ... if Found is still False, output the not-found message

# ---- main program ----
TheClub = Club("Riverside Juniors")
NewMember = Member("M4021", "Sara Ahmed", 16)
NewMember.AddCredits(20)
Added = TheClub.AddMember(NewMember)
# ... the same three lines for the other five rows of test data
# ... after Tom Reilly's AddMember, if Added is False, output the club-full message
print("Total credits: " + str(______))
# ... DisplayMember for "Lina Haddad", then for "Tom Reilly"
Exam conditions — where you need to be by the mock

Close this page. Set a timer for 25 minutes and work from the scenario box alone. Then, as an extension, the club gets a captain — a single contained object rather than an array:

Addition to ClubDescription
Captain : MemberA new attribute. There is no captain when the club is created.
SetCaptain(NewCaptain)Assigns the parameter to Captain.
GetCaptainName()Returns the name of the captain.

In the main program, make Omar Khalid the captain, then use GetCaptainName() to output Captain: Omar Khalid. It must appear in the same screenshot.

Before you screenshot

  • The first line of output should be Tom Reilly could not be added: club full. If the program crashes with IndexError instead, AddMember is not checking for space.
  • The total should be 150 — Tom's 10 credits are not counted, because he was never added. A total of 160, with Tom's details appearing at the end, means your array grew to fit a sixth member — usually because AddMember uses append instead of storing into the fixed array of five. A 'NoneType' error means a loop is running past the members you have.
  • You should see Lina's details once, and Tom Reilly not found once. If "not found" appears several times, it is inside the loop.
  • The screenshot must show the test data as well as the output — the lines of code that create the six members, alongside the console.
  • Your code goes into the document as text, never as a screenshot.

Self-marking

Mark scheme — open it only after you have attempted the task

Mark your own work honestly against this, then record your total in the Evidence Document.

Reveal the mark scheme [16 marks]
PointDescriptionMarks
(a) MP1Class Club declared with no parent class — class Club:, not class Club(Member):.1
(a) MP2Constructor takes exactly one parameter after self, assigned to a private ClubName.1
(a) MP3Members declared as a private array of five elements, its type evidenced as an array of Member (a comment is accepted).1
(a) MP4NumberOfMembers private and assigned 0 inside the constructor.1
(b) MP5AddMember() takes one parameter and checks there is space (NumberOfMembers < 5) before storing.1
(b) MP6…stores NewMember at index NumberOfMembers, then adds 1 to NumberOfMembers.1
(b) MP7Returns True when added and False when full — a Boolean on every path.1
(c) MP8Total initialised to 0 before a loop over the stored members only (up to NumberOfMembers).1
(c) MP9GetCredits() called on each stored member and the value returned added to the total.1
(c) MP10Total returned after the loop.1
(d) MP11GetName() of each stored member compared with SearchName, and DisplayDetails() called on the match.1
(d) MP12Not-found message output only after every stored member has been checked.1
(e) MP13A Club object created with "Riverside Juniors" in a variable not named after the class; each member created, credited and added with AddMember(), in order.1
(e) MP14The value returned by AddMember() stored and tested; club-full message output for Tom Reilly.1
(f) MP15Screenshot shows the test data being used, as well as the output.1
(f) MP16Output shows Tom Reilly could not be added: club full, Total credits: 150, Lina Haddad's details with 50 credits, and Tom Reilly not found.1

Accepted alternatives

  • MP5: testing for the full case first — if self.__NumberOfMembers == 5: return False — then storing.
  • MP11–MP12: a Found flag tested after the loop, or a return straight after DisplayDetails() with the not-found output after the loop.
  • MP13–MP14: six separate blocks of code, or a loop over a list of the test data — as long as the order is kept and AddMember's result is stored before it is tested.

Not credited, however good the code looks

  • class Club(Member): — MP1 lost, and MP2 with it if the constructor takes a member's parameters.
  • AddMember(self, MemberID, Name, Age) that builds the member inside the method — MP5 lost. The design passes a Member object.
  • No full check — MP5 lost, and the program crashes on Tom Reilly, so MP16 goes too.
  • TotalCredits() or DisplayMember() written as a function outside the class — no marks for that part. The design lists them as methods of Club.
  • Looping range(0, 5) over the array instead of up to NumberOfMembers — MP8 lost. It happens to work here because the club is full; on any club with fewer than five members it crashes on None.
  • self.__Members[Index].__Credits — MP9 lost. It crashes: the attribute is private to Member.
  • The not-found message in an else inside the loop — MP12 lost.
  • Calling AddMember without keeping what it returns, or calling it a second time inside the if — MP14 lost.
  • Code pasted as an image — no code marks at all.

Extension — not counted in the 16

  • E1 Captain is a private attribute with its type evidenced as Member, created in the constructor (None is accepted as "no captain").
  • E2 SetCaptain() takes one parameter and assigns it to Captain — attribute on the left.
  • E3 GetCaptainName() returns self.__Captain.GetName() — it asks the contained object, rather than reaching into it.
  • E4 Omar Khalid's object is passed to SetCaptain(), and the output reads Captain: Omar Khalid.

Hand-in

What to submit on Teams

One file: the Evidence Document for this lesson, saved as .docx and named Surname Initial — OOP Home Lesson 4.docx.

Checklist before you upload

  • Your name and class are at the top of the document.
  • Parts (a) to (e) are each pasted as text into their own box, with the indentation intact.
  • Your class header reads class Club: — nothing in brackets.
  • The screenshot shows the test data and the output: Tom Reilly refused, total 150, Lina's details, and Tom Reilly not found.
  • Your self-marked total out of 16 is filled in, with the points you lost named.
  • You are submitting the document only — not a zip, not the .py file.

Your next taught lesson opens by checking these submissions. If you got stuck, submit what you have with a note saying where.

Coming next

Home Lesson 5 — Inheritance. The other side of today's decision: a child class that is a parent class and more. Calling the parent's constructor with super().__init__ is a separate mark that examiners call a national weakness — and you will override a method for the first time.

Cambridge 9618 A Level Computer Science  ·  20.1 Programming Paradigms (OOP)  ·  Home Lesson 4 of 6  ·  Paper 4 Practical  ·  United School International