← All lessons · Cambridge 9618 A Level · Unit 20.1 · Paper 4 Practical
Work through this page, then turn the club into a class that contains its members, and submit the Evidence Document on Teams.
Start here
Same routine. This page teaches, then makes you type — and this week the club itself becomes a class that has its members inside it.
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.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
Answer these before you read anything else. Write them in your book, then reveal the answers and mark them.
Questions
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]else: return -1 inside its loop. State what goes wrong when the item being searched for is not in the first element. [1]Members : ARRAY[0:29] OF Member. State how many elements the array has, and the data type of each element. [2]| Q | Accepted | Marks |
|---|---|---|
| 1 | print(Team[3].GetName())Reject: Team[4] (past the end), Team.GetName(3), Team[3].__Name. | 1 |
| 2 | The 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 |
| 3 | MP1 — 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
| By the end of this lesson I can… | Syllabus | Paper |
|---|---|---|
| Decide from a class design whether two classes are related by containment or inheritance, and justify it with has a or is a. | 20.1 | 3 & 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.1 | 4 |
| Write methods of the containing class that work through the contained objects' own methods. | 20.1 | 4 |
| Write an add method that checks for space and returns a Boolean on every path. | 20.1 | 4 |
Vocabulary
| Term | Definition 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. |
| Inheritance | A 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
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.
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
Now run it and compare the real output with your prediction.
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.
886 1119
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.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
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.
| Sentence | True? | Relationship |
|---|---|---|
| A shelf is a book. | ✗ No | Containment — Shelf has an array of Book |
| A shelf has books. | ✓ Yes | |
| A novel is a book. | ✓ Yes | Inheritance — Novel is a child of Book |
| A club has members. | ✓ Yes | Containment |
| A savings account is an account. | ✓ Yes | Inheritance |
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 BookCaptain : 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().
A class design reads: Class Garage. Attributes: GarageName : STRING; Vehicles : ARRAY[0:9] OF Car. Which class header follows the design?
Garage a child of Car — it says a garage is a car. Car appears in the data type column of an attribute, so a garage has cars. This is exactly the error examiners have reported five times since 2022.Vehicles is the name of an attribute, not a class. Python stops with a NameError. The brackets after a class name are only ever for a parent class.Car appears in the data type column, so this is containment: the garage has an array of cars. The class needs no parent, so there are no brackets — the array is declared inside the constructor.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
class Shelf: — nothing in brackets, and so no super().__init__ anywhere.STRING and INTEGER. It is not a constructor parameter unless the design says so.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.
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 write | What 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.
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?
Team works through the player, never around it.__Goals is private to Player. Inside Team, Python renames it to _Team__Goals, which does not exist, so this crashes with an AttributeError. Call the get method.GetGoals, and a get method takes no parameter anyway. Pick the element first: self.__Players[Index].TypeError: missing 1 required positional argument: 'self'. Methods are called on objects.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
| Containment | Inheritance | |
|---|---|---|
| Sentence | A shelf has books | A novel is a book |
| Class header | class Shelf: | class Novel(Book): |
| Other class appears… | as an attribute's data type | in 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) |
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
| Class | Attributes | Relationship |
|---|---|---|
| Cinema | CinemaName : STRING Screens : ARRAY[0:4] OF Screen | Containment — a cinema has screens, so Screen appears as a data type. |
| Screen | ScreenNumber : INTEGER Seats : INTEGER | The parent class. |
| Screen3D | GlassesAvailable : INTEGER | Inheritance — 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
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
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.AddBook returns. When it is False, output the book's title followed by did not fit.Total pages: 1119 using TotalPages().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
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.True when it works and nothing at all when it does not hands the caller None.Added = Fiction.AddBook(...) and a test after each is equally good. The loop over a list of new books is just shorter.The marks
A music app stores playlists. The design says:
Class design — Playlist
| Attribute | Data type | Set when the object is created |
|---|---|---|
| PlaylistName | STRING | From a parameter |
| Songs | ARRAY[0:49] OF Song | Fifty empty elements |
| SongCount | INTEGER | Always 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 responseclass Playlist(Song): def __init__(self, PlaylistName, Title, Artist): super().__init__(Title, Artist) self.__PlaylistName = PlaylistName # STRING self.__SongCount = 0 # INTEGER
It runs. It still loses most of the declaration marks.
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.PlaylistName. Title and Artist belong to a song, and are only here because the candidate chose inheritance.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.
A design reads: "ElectricCar is a subclass of Car. It has one additional attribute, BatteryRange : INTEGER." Which of these follows the design?
Car any new attribute, and an ordinary car does not have electric cars. This invents a relationship that is not there.Assessed work
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.
| Attribute | Data type | Set when the object is created |
|---|---|---|
| ClubName | STRING | From a parameter |
| Members | ARRAY[0:4] OF Member | Five empty elements |
| NumberOfMembers | INTEGER | Always 0 |
| Method | Description |
|---|---|
| Constructor | Initialises 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:
| MemberID | Name | Age | Credits to add |
|---|---|---|---|
| "M4021" | "Sara Ahmed" | 16 | 20 |
| "M4022" | "Omar Khalid" | 17 | 45 |
| "M4023" | "Lina Haddad" | 16 | 60 |
| "M4024" | "Yusuf Rahman" | 18 | 0 |
| "M4025" | "Mia Chen" | 17 | 35 |
| "M4026" | "Tom Reilly" | 16 | 10 |
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
(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:
Club object with the name "Riverside Juniors"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 FALSETotalCredits(), with the label Total credits: 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]
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"
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 Club | Description |
|---|---|
| Captain : Member | A 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
Tom Reilly could not be added: club full. If the program crashes with IndexError instead, AddMember is not checking for space.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.Tom Reilly not found once. If "not found" appears several times, it is inside the loop.Self-marking
Mark your own work honestly against this, then record your total in the Evidence Document.
| Point | Description | Marks |
|---|---|---|
| (a) MP1 | Class Club declared with no parent class — class Club:, not class Club(Member):. | 1 |
| (a) MP2 | Constructor takes exactly one parameter after self, assigned to a private ClubName. | 1 |
| (a) MP3 | Members declared as a private array of five elements, its type evidenced as an array of Member (a comment is accepted). | 1 |
| (a) MP4 | NumberOfMembers private and assigned 0 inside the constructor. | 1 |
| (b) MP5 | AddMember() 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) MP7 | Returns True when added and False when full — a Boolean on every path. | 1 |
| (c) MP8 | Total initialised to 0 before a loop over the stored members only (up to NumberOfMembers). | 1 |
| (c) MP9 | GetCredits() called on each stored member and the value returned added to the total. | 1 |
| (c) MP10 | Total returned after the loop. | 1 |
| (d) MP11 | GetName() of each stored member compared with SearchName, and DisplayDetails() called on the match. | 1 |
| (d) MP12 | Not-found message output only after every stored member has been checked. | 1 |
| (e) MP13 | A 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) MP14 | The value returned by AddMember() stored and tested; club-full message output for Tom Reilly. | 1 |
| (f) MP15 | Screenshot shows the test data being used, as well as the output. | 1 |
| (f) MP16 | Output 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
if self.__NumberOfMembers == 5: return False — then storing.Found flag tested after the loop, or a return straight after DisplayDetails() with the not-found output after the loop.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.TotalCredits() or DisplayMember() written as a function outside the class — no marks for that part. The design lists them as methods of Club.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.else inside the loop — MP12 lost.AddMember without keeping what it returns, or calling it a second time inside the if — MP14 lost.Extension — not counted in the 16
Captain is a private attribute with its type evidenced as Member, created in the constructor (None is accepted as "no captain").SetCaptain() takes one parameter and assigns it to Captain — attribute on the left.GetCaptainName() returns self.__Captain.GetName() — it asks the contained object, rather than reaching into it.SetCaptain(), and the output reads Captain: Omar Khalid.Hand-in
One file: the Evidence Document for this lesson, saved as .docx and named Surname Initial — OOP Home Lesson 4.docx.
Checklist before you upload
class Club: — nothing in brackets..py file.Your next taught lesson opens by checking these submissions. If you got stuck, submit what you have with a note saying where.
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.