← All lessons · Cambridge 9618 A Level · Unit 20.1 · Paper 4 Practical
Five lessons in one question: a class, a child class, a container and a main program — against the clock, then marked and evidenced exactly as in June.
Start here
No new content today. This is the dress rehearsal: one Paper 4 question of 32 marks, at exam pace, followed by honest marking and a diagnosis of exactly which lesson to go back to.
Why 65 minutes
Paper 4 is 75 marks in 2 hours 30 minutes — about two minutes per mark. This question is worth 32, so 65 minutes is exam pace. If you cannot do it in one sitting, split it across two home periods: the timed attempt in one, the marking and evidence in the next. Do not split the timed attempt itself.
If the clock runs out, stop writing, note which part you reached in the Evidence Document, and then finish the program untimed. Both numbers are useful — the first tells you about pace, the second about knowledge.
Retrieval · real past-paper question
Question 1 is a real Cambridge Paper 3 question. Write your answers in your book, then reveal the mark scheme and mark them.
Questions
| OOP term | Description |
|---|---|
| Encapsulation | A methods used to return the value of a property |
| Getters | B the process of putting data and methods together as a single unit |
| Polymorphism | C methods used to update the value of a property |
| Setters | D allows methods to be redefined for derived classes |
| E enables the defining of a new class that inherits from a parent class |
Car's constructor takes Make and Model. The class Hatchback(Car) has the constructor header def __init__(self, Make, Model, Doors):. Write the first line of its body. [1]def GetAge(self, Age): return Age. State two things that are wrong with it. [2]| Q | Accepted | Marks |
|---|---|---|
| 1 | One mark for each correct match (Cambridge mark scheme): Encapsulation — B · Getters — A · Polymorphism — D · Setters — C. E is not used: it describes inheritance, which is not one of the four terms. A description left over is normal in a matching question — do not force it onto a term. | 4 |
| 2 | super().__init__(Make, Model) // Car.__init__(self, Make, Model)Reject: passing Doors to the parent; omitting the parameters. | 1 |
| 3 | Any two: it takes a parameter — a get method takes none (beyond self) // it returns the parameter, not the attribute — it should return self.__Age // it does not access the object's attribute at all. | 2 |
Q3 is the single most repeated OOP error in Paper 4 — a get method with a parameter appears in the examiner reports of all ten series since 2021. If you lost a mark on it, read Lesson 2 again before you start the clock.
Where this is going
| By the end of this lesson I can… | Syllabus | Paper |
|---|---|---|
| Complete a full Paper 4 OOP question — class, child class, container and main program — at exam pace. | 20.1 | 4 |
| Read a class design accurately: is a or has a; parameter or fixed value; returns or outputs. | 20.1 | 4 |
| Store and use the value a method returns, including one returned through a container. | 20.1 | 4 |
| Evidence a program to Paper 4 rules, and diagnose my lost marks against the lesson that teaches each one. | 20.1 | 4 |
Exam technique
Examiners keep saying the same thing: the strongest candidates are not the ones who know the most syntax. They are the ones who follow the design they are given. "Data structures differ each year to test if candidates can manipulate them when they are working in a slightly different way" (June 2023). So before you type a single line, spend five minutes with a pen.
super().__init__.Warm-up · untimed
Practise those five steps on a small design that is not today's question. Do not write the code; just answer the checkpoints.
Class design
| Parcel | Data type | Description |
|---|---|---|
| ParcelID | STRING | From a parameter |
| Weight | INTEGER | From a parameter |
| Delivered | BOOLEAN | Always FALSE when created |
| GetParcelID() | Returns ParcelID |
| ExpressParcel — a subclass of Parcel | Data type | Description |
|---|---|---|
| Deadline | STRING | From a parameter |
| Priority | INTEGER | Always 1 when created |
| Van | Data type | Description |
|---|---|---|
| Registration | STRING | From a parameter |
| Load | ARRAY[0:19] OF Parcel | Twenty empty elements |
| ParcelCount | INTEGER | Always 0 when created |
| LoadParcel(NewParcel) | Stores NewParcel in the next free element if there is space, and returns TRUE; otherwise returns FALSE | |
| FindParcel(SearchID) | Returns the index of the stored parcel whose ID is SearchID, or -1 if there is none |
How many parameters, after self, does the constructor of ExpressParcel take?
super().__init__. An express parcel still needs an ID and a weight — it is a parcel.Priority is always 1 — a fixed value, assigned inside the constructor. Taking a fixed value as a parameter is a mark lost in Lesson 1 and again here.Delivered and Priority are fixed values, so neither appears in the header — the parent's constructor sets Delivered, and the child sets Priority to 1 itself.Priority is always 1 when the object is created. It is assigned inside the constructor, not passed in.Load is declared as ARRAY[0:19] OF Parcel. Which of these can be passed to LoadParcel()?
ExpressParcel is a Parcel — it has everything a parcel has. Anywhere a Parcel is expected, a child object can go. That is exactly how Lesson 5's extension put a junior member into a club built for members.GetParcelID() on every element without caring which class each one is — and an overridden method would run in the right version for each. That is polymorphism doing real work.LoadParcel(NewParcel) — one parameter, and it is a parcel object. The caller creates the object and passes it. Passing the separate values instead was reported by examiners in November 2022 and June 2024.OF Parcel, so ordinary parcels certainly belong in it. Having more attributes does not make a class "more" acceptable — what matters is the is a relationship.Which version of FindParcel follows the design?
else returns -1 as soon as the first parcel does not match, so the loop never reaches the second. Not found can only be decided after every stored parcel has been checked.return -1 is in the right place, but the loop runs over all twenty elements. Unfilled elements hold None, and None.GetParcelID() crashes whenever the ID is not found and the van is not full. Loop to the counter.-1 is returned only once the loop has finished without one. A return on every path, in the right place.-1 every time, even when the parcel was found. Outputting where a return is required is reported in nine of the last ten exam series.Assessed work · timed
Question 1
A city runs a bike hire scheme from several hire stations. A program is to be written using Object-Oriented Programming. There are two kinds of bike: ordinary bikes, and electric bikes, which also have a battery. Each hire station stores up to 10 bikes.
The class Bike stores data about one bike.
| Bike | ||
|---|---|---|
| BikeID : STRING | the bike's unique ID, for example "B101" | |
| Model : STRING | the model of the bike | |
| HireRate : INTEGER | the cost of hiring the bike for one hour | |
| HoursHired : INTEGER | the total number of hours the bike has been hired for, initialised to 0 | |
| Available : BOOLEAN | TRUE if the bike can be hired, initialised to TRUE | |
| Constructor() | takes the bike ID, model and hire rate as parameters, and initialises all the attributes | |
| GetBikeID() | returns the bike ID | |
| GetAvailable() | returns the value of Available | |
| Hire(Hours) | if the bike is available: adds Hours to HoursHired, sets Available to FALSE and returns the cost, Hours multiplied by HireRate.If the bike is not available, returns -1 | |
The class EBike inherits from Bike.
| EBike | |
|---|---|
| BatteryLevel : INTEGER | the percentage of charge in the battery |
| Constructor() | takes the bike ID, model, hire rate and battery level as parameters. Calls the parent constructor and initialises BatteryLevel |
| Hire(Hours) | overrides the parent method. If BatteryLevel is less than 20, returns -1.Otherwise calls the parent's Hire() method. If the value it returns is not -1, reduces BatteryLevel by 10 for every hour hired. Returns the value returned by the parent's method. |
The class HireStation stores the bikes at one station.
| HireStation | |
|---|---|
| StationName : STRING | the name of the station |
| Bikes : ARRAY[0:9] OF Bike | the bikes at the station, initially empty |
| NumberOfBikes : INTEGER | the number of bikes stored, initialised to 0 |
| Constructor() | takes the station name as a parameter and initialises all the attributes |
| AddBike(NewBike) | if there is space, stores NewBike in the next free element of Bikes, adds 1 to NumberOfBikes and returns TRUE. Otherwise returns FALSE |
| HireBike(BikeID, Hours) | searches the stored bikes for the bike with the ID BikeID. If it is found, hires it for Hours hours using its Hire() method and returns the value that Hire() returns. If no stored bike has that ID, returns -1 |
| CountAvailable() | returns the number of stored bikes that are available |
(a) Write program code to declare the class Bike and its constructor. Use your programming language's appropriate constructor. All attributes must be private. If you are writing in Python, include attribute declarations using comments. [4]
(b) Write program code for the methods GetBikeID() and GetAvailable(). [2]
(c) Write program code for the method Hire() in the class Bike. [4]
(d) Write program code to declare the class EBike and its constructor. [3]
(e) Write program code for the method Hire() in the class EBike. [4]
(f) Write program code to declare the class HireStation, its constructor and the method AddBike(). [4]
(g) Write program code for the method HireBike(). [4]
(h) Write program code for the method CountAvailable(). [2]
(i) Write the main program. It must:
HireStation object with the name "Harbour Road"AddBike()HireBike(), store the value it returns, and output either<BikeID> hired for <Hours> hours, cost <value> or <BikeID> cannot be hired if the value is -1Bikes available: followed by the value returned by CountAvailable(). [3]| Class | BikeID | Model | Rate | Battery |
|---|---|---|---|---|
| Bike | "B101" | "City Cruiser" | 4 | — |
| Bike | "B102" | "Trail Runner" | 5 | — |
| EBike | "E201" | "Volt 500" | 9 | 85 |
| EBike | "E202" | "Volt 500" | 9 | 15 |
| Bike | "B103" | "City Cruiser" | 4 | — |
| Request | BikeID | Hours |
|---|---|---|
| 1 | "B102" | 3 |
| 2 | "E201" | 2 |
| 3 | "E202" | 1 |
| 4 | "B102" | 2 |
| 5 | "B777" | 1 |
(j) Test your program. Take a screenshot showing the output. [2]
In the exam you would move on, and parts are marked independently — so move on here too. If you open a hint, write "hint used" beside that part in the Evidence Document. Honesty is what makes the diagnosis work.
self.return — nothing after a return runs.super().__init__.Cost = super().Hire(Hours) on a line of its own — then test Cost.Club, with different names and ten elements.Hire() returns; return -1 only after the loop.NumberOfBikes, not to 10. Ask each bike with GetAvailable().Station.AddBike(EBike("E201", "Volt 500", 9, 85)) creates the object and passes it in one line.Press Run to execute.
Use your own IDE if you can — the exam does. This runner is here if you have nothing else, and it saves your code in this browser as you type. Paste into the Evidence Document from whichever you used.
Add a get method GetBatteryLevel() to EBike. In the main program, keep the E201 object in a variable before you add it to the station, and after the other output, use it to output E201 battery: followed by its battery level.
Then explain in one sentence, in the Evidence Document, why the station's copy of E201 and your variable show the same battery level.
B102 hired for 3 hours, cost 15 E201 hired for 2 hours, cost 18 E202 cannot be hired B102 cannot be hired B777 cannot be hired Bikes available: 3
With the extension, a seventh line: E201 battery: 65. If yours differs, the difference tells you where to look:
| You see | Look at |
|---|---|
E202 hired for 1 hours, cost 9 and Bikes available: 2 | (e) — the battery check is missing. E202 has only 15%. |
B102 hired for 2 hours, cost 10 | (c) — Hire() is not setting Available to False, so B102 is hired twice. |
Every request cannot be hired | (g) — return -1 is inside the loop, so only the first bike, B101, is ever checked. None of the requests is for B101. |
'NoneType' object has no attribute … | (g) or (h) — a loop runs to 10 instead of to NumberOfBikes. Only five bikes are stored. |
AttributeError: … '_Bike__…' | (d) — the parent's constructor is not being called. |
AttributeError: … '_EBike__Available' or similar | (e) — the child is reaching into the parent's private attributes. Let super().Hire(Hours) do that work. |
cost None on a line | (c), (e) or (g) — a path through one of them has no return, or a value returned is not passed on. |
Self-marking
Mark strictly. A mark point is a behaviour: if your code does not do it, you do not get it, however close it looks. Record each point in the Evidence Document.
| Point | Description | Marks |
|---|---|---|
| (a) MP1 | BikeID, Model and HireRate declared private, with their data types evidenced. | 1 |
| (a) MP2 | Constructor takes exactly three parameters and assigns them to those attributes — attribute on the left. | 1 |
| (a) MP3 | HoursHired private, typed, assigned 0 in the constructor. | 1 |
| (a) MP4 | Available private, typed, assigned True in the constructor. | 1 |
| (b) MP5 | GetBikeID() — no parameter, returns BikeID. | 1 |
| (b) MP6 | GetAvailable() — no parameter, returns Available. | 1 |
| (c) MP7 | Hire() takes one parameter and tests whether the bike is available. | 1 |
| (c) MP8 | …when available, adds Hours to HoursHired and sets Available to False… | 1 |
| (c) MP9 | …and returns Hours * HireRate. | 1 |
| (c) MP10 | Returns -1 when not available. | 1 |
| (d) MP11 | Class header shows inheritance from Bike. | 1 |
| (d) MP12 | Constructor takes four parameters and calls the parent constructor with the first three. | 1 |
| (d) MP13 | BatteryLevel private, typed, assigned from the parameter. | 1 |
| (e) MP14 | Hire() defined in EBike with one parameter, returning -1 when BatteryLevel < 20. | 1 |
| (e) MP15 | Otherwise calls the parent's Hire(Hours) and stores the value it returns. | 1 |
| (e) MP16 | Reduces BatteryLevel by 10 * Hours only when that value is not -1. | 1 |
| (e) MP17 | Returns the stored value. | 1 |
| (f) MP18 | class HireStation: — no parent — with StationName private, from the one parameter. | 1 |
| (f) MP19 | Bikes a private array of 10 elements typed as Bike, and NumberOfBikes private and set to 0. | 1 |
| (f) MP20 | AddBike() checks for space, stores at index NumberOfBikes, then adds 1. | 1 |
| (f) MP21 | Returns True / False on every path. | 1 |
| (g) MP22 | Loops over the stored bikes only. | 1 |
| (g) MP23 | Compares each bike's GetBikeID() with the parameter. | 1 |
| (g) MP24 | Calls Hire(Hours) on the matching bike and returns the value it returns. | 1 |
| (g) MP25 | Returns -1 only after the loop. | 1 |
| (h) MP26 | Count set to 0, loop over stored bikes, GetAvailable() tested and count incremented. | 1 |
| (h) MP27 | Count returned after the loop. | 1 |
| (i) MP28 | Station created; the five bikes created as the right classes with the test data, each added with AddBike(). | 1 |
| (i) MP29 | Each request made with HireBike(); value returned stored, tested against -1, and the right message output in each case. | 1 |
| (i) MP30 | Bikes available: output with the value returned by CountAvailable(). | 1 |
| (j) MP31 | Screenshot shows the program running with the test data. | 1 |
| (j) MP32 | Output matches the six expected lines. | 1 |
Accepted alternatives
Bike.__init__(self, BikeID, Model, HireRate) and Bike.Hire(self, Hours).-1 in the else.while loop with a found flag, provided the value returned by Hire() is the value returned by HireBike().Not credited, however good the code looks
HoursHired, Available or a fixed value taken as a constructor parameter — MP2 lost, as well as the point for that attribute.print(Hours * self.__HireRate) in place of return — MP9 lost, and everything that relies on the value fails with it.Available after the return line — MP8 lost. That line never runs.super().Hire(Hours) called inside an if condition and again on the next line — MP15 lost. The bike is hired twice: the second call finds it unavailable and returns -1.self.__Available or self.__HireRate anywhere in EBike — MP15 lost. They are private to Bike.class HireStation(Bike): — MP18 lost. Bike is in the data type column: a station has bikes.HireBike() that outputs a message instead of returning — MP24 lost. The design says returns, and part (i) depends on it.HireBike() without storing what it returns, then calling it again to print the cost — MP29 lost, and the second call reports the bike as unavailable.# 1(a) class Bike: def __init__(self, BikeID, Model, HireRate): self.__BikeID = BikeID # STRING self.__Model = Model # STRING self.__HireRate = HireRate # INTEGER self.__HoursHired = 0 # INTEGER self.__Available = True # BOOLEAN # 1(b) def GetBikeID(self): return self.__BikeID def GetAvailable(self): return self.__Available # 1(c) def Hire(self, Hours): if self.__Available == True: self.__HoursHired = self.__HoursHired + Hours self.__Available = False return Hours * self.__HireRate else: return -1 # 1(d) an EBike IS A Bike class EBike(Bike): def __init__(self, BikeID, Model, HireRate, BatteryLevel): super().__init__(BikeID, Model, HireRate) self.__BatteryLevel = BatteryLevel # INTEGER # 1(e) overrides Bike.Hire def Hire(self, Hours): if self.__BatteryLevel < 20: return -1 Cost = super().Hire(Hours) # store what the parent's method returns if Cost != -1: self.__BatteryLevel = self.__BatteryLevel - 10 * Hours return Cost # 1(f) a HireStation HAS bikes — no brackets class HireStation: def __init__(self, StationName): self.__StationName = StationName # STRING self.__Bikes = [None] * 10 # ARRAY[0:9] OF Bike self.__NumberOfBikes = 0 # INTEGER def AddBike(self, NewBike): if self.__NumberOfBikes < 10: self.__Bikes[self.__NumberOfBikes] = NewBike self.__NumberOfBikes = self.__NumberOfBikes + 1 return True else: return False # 1(g) def HireBike(self, BikeID, Hours): for Index in range(0, self.__NumberOfBikes): if self.__Bikes[Index].GetBikeID() == BikeID: return self.__Bikes[Index].Hire(Hours) return -1 # 1(h) def CountAvailable(self): Count = 0 for Index in range(0, self.__NumberOfBikes): if self.__Bikes[Index].GetAvailable() == True: Count = Count + 1 return Count # 1(i) main program Station = HireStation("Harbour Road") Added = Station.AddBike(Bike("B101", "City Cruiser", 4)) Added = Station.AddBike(Bike("B102", "Trail Runner", 5)) Added = Station.AddBike(EBike("E201", "Volt 500", 9, 85)) Added = Station.AddBike(EBike("E202", "Volt 500", 9, 15)) Added = Station.AddBike(Bike("B103", "City Cruiser", 4)) Requests = [["B102", 3], ["E201", 2], ["E202", 1], ["B102", 2], ["B777", 1]] for Request in Requests: Cost = Station.HireBike(Request[0], Request[1]) if Cost == -1: print(Request[0] + " cannot be hired") else: print(Request[0] + " hired for " + str(Request[1]) + " hours, cost " + str(Cost)) print("Bikes available: " + str(Station.CountAvailable()))
HireBike calls Hire() on whatever is in the array. For E201 and E202 that is EBike's version, with the battery check; for the others it is Bike's. The station never asks which class a bike is.Bike.Hire returns the cost to EBike.Hire, which stores it and returns it to HireStation.HireBike, which returns it to the main program, which stores it and tests it. Break any link and the output fails — that chain is exactly what examiners mean by "using what a call returns".Added = … even though nothing is tested here. Keeping what a method returns is a habit; in a question that asked for a club-full message, it would be the mark.After the marking
Find every mark point you lost in this table. The lesson that appears most often is the one to redo — the whole page, Exam Conditions tier included — before the mock.
| Mark points | What they test | Go back to |
|---|---|---|
| MP1–MP4, MP13 | Private attributes, data types, fixed values vs parameters | Lesson 1 — Classes, Attributes and the Constructor |
| MP5–MP10 | Get methods without parameters; returning, not outputting | Lesson 2 — Get and Set Methods |
| MP22–MP30 | Looping over objects, searching, storing what a call returns | Lesson 3 — Using Objects |
| MP18–MP21 | Has a: an array of objects inside a class, with a counter | Lesson 4 — Containment or Inheritance? |
| MP11–MP12, MP14–MP17 | Is a: the parent constructor call, overriding, the parent's method | Lesson 5 — Inheritance |
| MP31–MP32 | Evidence: test data and output in the screenshot | The marking rules on page 1 of every Evidence Document |
Pace or knowledge?
Compare the part you reached when the clock stopped with your final mark. Lots of marks but an unfinished question means pace: practise the Exam Conditions tiers with a timer. An unfinished question with marks lost in the parts you did write means knowledge: redo the lesson the table points to. Many people have a bit of both — write down which matters more for you.
Hand-in
One file: the Evidence Document for this lesson, saved as .docx and named Surname Initial — OOP Home Lesson 6.docx.
Checklist before you upload
.py file.Your next taught lesson opens by going through the most common lost marks across the class. If you ran out of time, submit what you have — the unfinished parts are useful evidence too.
That completes the OOP arc. In the exam, an OOP question usually sits alongside one on file handling and one on an abstract data type — and the OOP question itself often loads its objects from a text file. Those topics are next on the practical strand, and everything from these six lessons carries straight into them.