Design a class with UML, write it in Python and Java with a constructor, accessors, mutators and static members, apply encapsulation and information hiding, and evaluate object-oriented programming.
Each is in the Computer Science glossary, with every other term in the subject.
A keyword that sets which parts of a program may use a class member, such as private (only code in the same class) or public (any code).
A public method that returns the value of an object's attribute without changing it, usually named getX() (Java) or get_x() (Python).
A relationship between two classes in which objects of one class are connected to, and can use, objects of the other, drawn in UML as a solid line between the classes.
A named item of data that a class declares and each object holds a value for.
A template that defines the attributes and methods shared by every object of one kind.
A special method that runs automatically when an object is created and sets the initial values of the object's attributes.
The bundling of an object's data and the methods that operate on that data into a single unit, the class.
With a free account, Augment remembers where you stopped and what you have practiced.
The principle that a class keeps its data and implementation details inaccessible to other code, which can use the object only through the methods the class chooses to make public.
The mechanism by which a new class is defined from an existing class, automatically gaining its attributes and methods and adding or changing some of its own.
An object considered as a member of the class it was created from.
A method that is called on a particular object and can read and change that object's instance variables through its self reference.
An attribute of which each object holds its own separate copy, so that its value can differ from object to object.
The process of creating a new object from a class, which allocates memory for the object and runs its constructor.
A subprogram defined in a class that describes a behaviour of its objects and can use and change their attributes.
The number of objects of one class that can be linked to a single object of the associated class, written at the end of the association line as a number or a range.
A public method that changes the value of an object's attribute, normally after checking that the new value is valid, usually named setX() (Java) or set_x() (Python).
A special reference value, null in Java and None in Python, meaning that a variable does not refer to any object.
A single entity in a program, created from a class, that holds its own values for the class's attributes and can carry out the class's methods.
A programming paradigm in which a program is built from objects, each combining data with the methods that act on that data, and each created from a class that describes its kind.
The ability of one method name or operator to be used with objects of different types, each type carrying out the operation in its own way.
A style of programming, defined by the way a program's data and instructions are organised and by the concepts a programmer uses to structure the solution.
The set of public methods (and any public attributes) through which other code can use the objects of a class, as opposed to the hidden details of how the class works.
A value that identifies where an object is stored in memory, held by a variable so that the program can reach the object.
The name a method uses to refer to the object on which it was called, written self in Python and this in Java.
The set of values held by an object's attributes at a particular moment.
A method that belongs to the class rather than to an object, is called through the class name and has no self reference, so it cannot use instance variables directly.
A variable that belongs to the class rather than to any object, so that a single copy is shared by all objects of the class.
A diagram in the Unified Modeling Language that shows classes as boxes listing their attributes and methods, and the relationships between the classes as lines.
Describe classes, objects, inheritance, encapsulation and polymorphism, using them to model real-world entities.
Evaluate the advantages and disadvantages of object-oriented programming in different programming scenarios.
Construct a design of classes, their attributes and methods from a set of requirements, using UML class diagrams.
Construct code that defines a class, uses a constructor to set an object's initial state and creates objects from it.
Distinguish between static and non-static variables and methods, and decide when to use each.
Explain the principles of encapsulation and information hiding.
Apply access modifiers, accessors and mutators to control access to an object's state.
Large programs describe many things at once: customers, bank accounts, enemies in a game, books in a library, sensors in a greenhouse. Object-oriented programming organises a program around these things. Each one is represented by an object that holds its own data and carries the operations that act on that data, and each kind of object is described once, in a class. The idea dates from the 1960s and is now built into both course languages, Java and Python, and into most software that students meet, from mobile apps to game engines.
A single well-designed class already shows most of the ideas that matter: how an object gets its starting values, how two objects of the same class keep separate data, which data belongs to the class as a whole, and how the class protects its data from careless or harmful changes. A bank account is a convenient model for all of them. It has data that differs between customers (an owner and a balance), data shared by the whole bank (an interest rate) and rules that must never be broken (no negative balance), so a BankAccount class, written in Python and Java and drawn as a UML class diagram, shows each idea in turn.
YouTubePrograms written in the style of subtopic B2.3, Programming constructs, are sequences of statements grouped into subprograms. The data sits in separate variables and lists, and any subprogram that receives the data can change it. This style is called procedural programming. It works well for short programs, but in a large one the connection between a piece of data and the code allowed to change it exists only in the programmer's head.
Object-oriented programming takes a different starting point. A procedural design begins with the steps the program performs; an object-oriented design begins with the things the program deals with, the data each one holds and the actions each one can carry out. Each thing becomes an object, and the code that changes an object's data is kept together with that data.
The two paradigms are not opposites. An object's methods are themselves written with sequence, selection and iteration, so procedural skills are still needed inside every class. Python lets a programmer mix the two freely: a script can define no classes at all. Java is stricter, because every method must belong to a class, even the main method that starts a program.
A class is a description of a kind of thing. It lists the data every object of that kind holds and the operations every object of that kind can perform, but it holds no actual data about any particular thing. An object is one particular thing made from that description, with its own data.
The usual comparison is an architect's plan and the houses built from it. One plan can produce a whole street of houses. Each house has its own address, its own paint colour and its own furniture, yet every one has the rooms the plan describes. Painting one house does not change the plan or the other houses.
An object made from a class is called an instance of that class, and the act of making it is instantiation. "Object" and "instance" name the same thing from two angles: "object" stresses that it exists and holds data, and "instance" stresses which class it was made from ("Amira's account is an instance of BankAccount").
Every object has two sides. The data it holds is described by its attributes, and the values those attributes hold at a given moment make up its state. What it can do is described by its methods, which together form its behaviour.
An attribute is declared with a data type in Java (double balance;), while in Python it comes into existence the first time a value is assigned to it (self.balance = 0.0). Either way, each object holds its own value.
Methods are subprograms defined inside a class. They are called on an object with dot notation, such as account.deposit(30.0), and they can read and change that object's attributes. A method that changes the state of an object is how an OOP program does its work: instead of a separate function that is handed a balance, the account itself is asked to deposit money.
Designing a class starts from the thing being modelled. A bank account in the real world has an account number, an owner and a balance; money can be paid in, taken out and earn interest. The class keeps only the details that matter to the program: the account's colour on a banking app or the branch where it was opened are left out. Choosing what to keep and what to leave out is abstraction in the computational-thinking sense of subtopic B1.1.
Real-world bank account | In the | Kind of member |
|---|---|---|
Account number printed on the card |
| Attribute |
Name of the account holder |
| Attribute |
Money in the account |
| Attribute |
Paying money in |
| Method |
Taking money out |
| Method |
Interest added by the bank |
| Method |
The first version of BankAccount has two attributes, a constructor that gives them their starting values, and two methods. The program after the class creates two accounts and uses them.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance = self.balance + amount
def describe(self):
return self.owner + " has " + str(self.balance)
first = BankAccount("Amira", 120.0)
second = BankAccount("Kenji", 45.5)
first.deposit(30.0)
print(first.describe())
print(second.describe())class BankAccount {
String owner;
double balance;
BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
void deposit(double amount) {
balance = balance + amount;
}
String describe() {
return owner + " has " + balance;
}
}
public class Bank {
public static void main(String[] args) {
BankAccount first = new BankAccount("Amira", 120.0);
BankAccount second = new BankAccount("Kenji", 45.5);
first.deposit(30.0);
System.out.println(first.describe());
System.out.println(second.describe());
}
}Both versions print the same two lines. Only first received the deposit, so the two objects now hold different states even though they come from one class.
Amira has 150.0
Kenji has 45.5The two languages lay out a class differently. Java declares every attribute with its type at the top of the class, gives each method a return type (void when nothing is returned), and creates an object with the keyword new. Python has no attribute declarations: the attributes are created inside __init__ by assigning to self.owner and self.balance, and an object is created by calling the class like a function, BankAccount("Amira", 120.0), with no new. Class names use PascalCase in both languages; Python methods use snake_case and Java methods camelCase.
An object must start life in a sensible state: an account with no owner or with a missing balance would break any method that uses them. A constructor is the special method that runs automatically when an object is created and sets the starting values of its attributes.
Creating an object therefore happens in two steps. First, memory is set aside for a new object with room for each attribute. Then the constructor runs on that new object, receiving the arguments given in the call and copying them into the attributes. Only after the constructor finishes is a reference to the finished object handed back and stored in a variable.
A constructor does more than copy parameters. It can set attributes that the caller does not supply (a new account's transaction count starts at 0), give a default value, or reject impossible values, so that no object can ever exist in an invalid state. The final version of BankAccount refuses a negative opening balance in its constructor for this reason.
self and thisInside a method, the code must be able to say "this particular object". Many accounts share the same deposit method, so when first.deposit(30.0) runs, the method needs to know that first's balance is the one to change.
Python makes the reference visible. Every instance method has self as its first parameter, and the call first.deposit(30.0) is carried out as BankAccount.deposit(first, 30.0): the object before the dot becomes self. Every attribute access inside the class must therefore be written self.balance; a bare balance would be a new local variable. Java passes the reference automatically and hides the parameter. Inside a Java method, balance on its own means this.balance, unless a parameter or local variable has the same name.
That exception matters in constructors, where parameters are usually given the same names as the attributes they fill. In this.owner = owner;, the left side is the object's attribute and the right side is the parameter. Writing owner = owner; instead assigns the parameter to itself and leaves the attribute untouched, a form of shadowing.
Java's default values explain why the Java version fails silently. Every attribute of a new Java object starts with a default (0 for numbers, false for boolean, null for objects such as a String) before the constructor runs, so a forgotten assignment produces a wrong value rather than an error. Python has no defaults: an attribute exists only once something assigns to it.
A bank may open an account with an opening deposit or with nothing. Java handles this with constructor overloading: a class can have several constructors with the same name and different parameter lists, and the compiler picks the one whose parameters match the arguments. One constructor can call another with this(...), so the setting-up code is written once. Overloading in general is HL content (method overloading, subtopic B3.2), but overloaded constructors are part of writing an ordinary class.
Python allows only one __init__ per class. A second def __init__ does not add a second constructor; it replaces the first. The Python way to allow different calls is a default parameter value, as in def __init__(self, owner, balance=0.0), which lets the caller leave out the balance.
Java | Python | |
|---|---|---|
Constructors per class | Any number, with different parameter lists | Exactly one |
Allowing a missing balance | A second constructor | A default value |
Reusing set-up code |
| Not needed: one method handles both calls |
A second definition with the same parameters | Compile error | Silently replaces the first |
The next version of the class uses both techniques and adds a method that turns an object into readable text. Python calls __str__ automatically when an object is printed or passed to str(); Java calls toString() automatically when an object is printed or joined to a string. Without them, printing an object shows only its class and a memory-related code, such as <__main__.BankAccount object at 0x…> or BankAccount@1b6d3586.
class BankAccount:
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance = self.balance + amount
def __str__(self):
return self.owner + " has " + str(self.balance)
accounts = [BankAccount("Amira", 120.0), BankAccount("Kenji", 45.5), BankAccount("Sofia")]
accounts[2].deposit(80.0)
total = 0.0
for account in accounts:
print(account)
total = total + account.balance
print("Total held:", total)import java.util.ArrayList;
class BankAccount {
String owner;
double balance;
BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
BankAccount(String owner) {
this(owner, 0.0);
}
void deposit(double amount) {
balance = balance + amount;
}
public String toString() {
return owner + " has " + balance;
}
}
public class Bank {
public static void main(String[] args) {
ArrayList<BankAccount> accounts = new ArrayList<BankAccount>();
accounts.add(new (, ));
accounts.add( (, ));
accounts.add( ());
accounts.get().deposit();
;
(BankAccount account : accounts) {
System.out.println(account);
total = total + account.balance;
}
System.out.println( + total);
}
}Amira has 120.0
Kenji has 45.5
Sofia has 80.0
Total held: 245.5Sofia's account was created with the one-argument constructor (Java) or the default parameter (Python), so it started at 0.0 before the deposit of 80.0.
The program also shows that objects can be stored like any other value. A Python list, a Java array (BankAccount[] accounts = new BankAccount[3];) or a Java ArrayList (ArrayList<BankAccount>) can hold many objects of one class, and a loop can visit each one and call its methods. The total is found with an ordinary running-total loop rather than the built-in sum, so the same method works in any language and whenever built-in functions are not available.
A loop over objects is how most OOP programs process data: "for every account, add interest", "for every enemy, move one step", "count the lockers that are free". The loop only calls each object's methods, so it does not depend on how each object stores its data.
A variable of a class type does not contain an object. It contains a reference: information that tells the program where the object is stored in memory. The object itself lives in a separate area of memory, and any number of variables can refer to it.
The difference shows as soon as one variable is assigned to another. With integers, y = x copies the number, and changing y leaves x alone. With objects, b = a copies only the reference: a and b are two names for one account, and a deposit made through either name is seen through both.
A variable can also hold a reference that points at no object at all. Python writes this value None; Java writes it null. It is useful as a marker for "nothing here yet" (a free locker has no owner), but calling a method through it stops the program with a runtime error.
The program creates two accounts, copies one reference, changes the account through the copy, and then sets the third variable to the null reference.
class BankAccount:
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance = self.balance + amount
a = BankAccount("Amira", 100.0)
b = a # copies the reference, not the object
c = BankAccount("Amira", 100.0) # a second, separate object
b.deposit(50.0)
print(a.balance, b.balance, c.balance)
print(a is b, a is c)
c = None # c no longer refers to any object
print(c is None)
c.deposit(10.0)class BankAccount {
String owner;
double balance;
BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
void deposit(double amount) {
balance = balance + amount;
}
}
public class References {
public static void main(String[] args) {
BankAccount a = new BankAccount("Amira", 100.0);
BankAccount b = a; // copies the reference, not the object
BankAccount c = new (, );
b.deposit();
System.out.println(a.balance + + b.balance + + c.balance);
System.out.println((a == b) + + (a == c));
c = ;
System.out.println(c == );
c.deposit();
}
}150.0 150.0 100.0
True False
True
AttributeError: 'NoneType' object has no attribute 'deposit'The Java version prints true false and true, then stops with a NullPointerException whose message says that BankAccount.deposit(double) cannot be invoked because the reference is null. The reference diagram shows the state just before that last line.

Two details follow from this picture. First, a is c (Python) and a == c (Java) are false even though both accounts hold the same owner and balance. These operators compare references, so they are true only when two variables refer to one and the same object; comparing the data inside two objects means comparing their attributes, as in a.balance == c.balance. Second, an object that no variable refers to can never be used again. Both Python and Java find such objects and reclaim their memory automatically, a process called garbage collection, so the programmer never frees object memory by hand.
Every attribute in the classes so far belongs to one object: each account has its own owner and its own balance. Some data, however, describes the whole class rather than any one object. A bank sets one interest rate for all its accounts, and it needs one counter of how many accounts have been opened. Storing a copy of the interest rate in every account would waste memory and, worse, allow the copies to disagree.
A static variable is stored once, in the class itself, and is shared by every object of the class. It exists as soon as the class is loaded, before any object has been created, and it keeps its value however many objects are made.
Methods divide in the same way. An instance method is called on one object and works with that object's attributes, so it has a self reference. A static method is called on the class itself and has no particular object to work with, so it has no self or this.
Static methods are used for tasks that need no object's data: checking that an amount is valid, converting a value, or reading and updating a static variable. They are called with the class name before the dot, BankAccount.isValidAmount(20), which makes it plain that no object is involved.
The next version of BankAccount has both kinds of member. Each new account takes the next number from a shared counter; interest_rate is shared by all accounts; and is_valid_amount checks an amount without needing an account.
class BankAccount:
interest_rate = 0.02 # class variable: one copy, shared by every account
accounts_opened = 0 # class variable: counts objects created
def __init__(self, owner, balance=0.0):
BankAccount.accounts_opened = BankAccount.accounts_opened + 1
self.number = BankAccount.accounts_opened # instance variables:
self.owner = owner # one copy per object
self.balance = balance
def add_interest(self): # instance method
self.balance = self.balance + self.balance * BankAccount.interest_rate
@staticmethod
():
amount >
first = BankAccount(, )
second = BankAccount(, )
(BankAccount.accounts_opened, first.number, second.number)
first.add_interest()
second.add_interest()
(first.balance, second.balance)
BankAccount.interest_rate =
first.add_interest()
second.add_interest()
(first.balance, second.balance)
(BankAccount.is_valid_amount(-), BankAccount.is_valid_amount())class BankAccount {
static double interestRate = 0.02; // class variable: one copy, shared by every account
static int accountsOpened = 0; // class variable: counts objects created
int number; // instance variables:
String owner; // one copy per object
double balance;
BankAccount(String owner, double balance) {
accountsOpened = accountsOpened + 1;
this.number = accountsOpened;
.owner = owner;
.balance = balance;
}
{
balance = balance + balance * interestRate;
}
{
amount > ;
}
}
{
{
(, );
(, );
System.out.println(BankAccount.accountsOpened + + first.number + + second.number);
first.addInterest();
second.addInterest();
System.out.println(first.balance + + second.balance);
BankAccount.interestRate = ;
first.addInterest();
second.addInterest();
System.out.println(first.balance + + second.balance);
System.out.println(BankAccount.isValidAmount(-) + + BankAccount.isValidAmount());
}
}2 1 2
102.0 255.0
106.08 265.2
False TrueJava prints false true on the last line. The first interest payment adds 2% to each balance ( and ). A single assignment then changes the shared rate to 4%, and the second payment uses it for both accounts:

A static method runs without an object, so it has no balance to look at. If the static method canAfford tries to read balance, the Java compiler refuses the class before it can run:
StaticError.java:5: error: non-static variable balance cannot be referenced from a static context
return balance >= price;
^Python has no compiler check, but a static method has no self parameter, so self.balance inside it fails with NameError when it runs. The fix is either to make the method an instance method (if it needs one account's data) or to pass the data in as a parameter (if it does not). The reverse direction is always allowed: an instance method can read and change static variables, as add_interest does with the interest rate and the constructor does with the counter.
Python has two kinds of method that belong to the class. A method marked @staticmethod receives no automatic first parameter at all. A method marked @classmethod receives the class itself as its first parameter, named cls by convention, so it can read and change class variables through cls without writing the class name. Neither has a self, so neither can use instance variables, and both are called through the class name. Java has a single static keyword that covers both uses.
class BankAccount:
interest_rate = 0.02
@classmethod
def set_interest_rate(cls, rate): # cls is the class itself
cls.interest_rate = rate
@staticmethod
def is_valid_amount(amount): # no automatic first parameter
return amount > 0
BankAccount.set_interest_rate(0.03)
print(BankAccount.interest_rate, BankAccount.is_valid_amount(0))class BankAccount {
static double interestRate = 0.02;
static void setInterestRate(double rate) { // Java has only static methods
interestRate = rate;
}
static boolean isValidAmount(double amount) {
return amount > 0;
}
}
public class Rates {
public static void main(String[] args) {
BankAccount.setInterestRate(0.03);
System.out.println(BankAccount.interestRate + " " + BankAccount.isValidAmount(0));
}
}The Python version prints 0.03 False and the Java version 0.03 false: the shared rate has changed for the whole class, and an amount of 0 is not a valid deposit.
Math classJava's standard library contains a class built entirely from static members. Every public member of java.lang.Math is static: constants such as Math.PI and methods such as Math.sqrt, Math.pow, Math.max and Math.round. None of them needs an object, because the square root of 2 is the same whoever asks. The class goes further: its constructor is private, so new Math() is refused with error: Math() has private access in Math, and no Math object can ever exist. Python puts the same tools in the math module, which is a collection of functions rather than a class.
import math
radius = 3.0
area = math.pi * radius ** 2
print(math.sqrt(2.0))
print(round(area, 2))
print(max(17, 42))public class MathDemo {
public static void main(String[] args) {
double radius = 3.0;
double area = Math.PI * Math.pow(radius, 2);
System.out.println(Math.sqrt(2.0));
System.out.println(Math.round(area * 100) / 100.0);
System.out.println(Math.max(17, 42));
}
}1.4142135623730951
28.27
42The main method of every Java program is static for the same reason: when the program starts, no object exists yet, so the first method to run must belong to a class, not to an object.
Python reads attributes in a fixed order. first.interest_rate first looks for an instance variable called interest_rate on the object; only if there is none does it fall back to the class attribute. Assigning through an object, however, always writes to the object. So first.interest_rate = 0.05 does not change the shared rate: it creates a new instance variable on first alone, which from then on hides the class attribute for that one object.
class BankAccount:
interest_rate = 0.02
def __init__(self, owner):
self.owner = owner
first = BankAccount("Amira")
second = BankAccount("Kenji")
first.interest_rate = 0.05 # creates a new instance variable on first only
print(first.interest_rate, second.interest_rate, BankAccount.interest_rate)
BankAccount.interest_rate = 0.03 # changes the class variable
print(first.interest_rate, second.interest_rate, BankAccount.interest_rate)// Java has no equivalent trap: an object cannot gain a new field at run time.
// first.interestRate = 0.05; compiles, but it changes the one static field shared by every account,
// so Java programmers write BankAccount.interestRate = 0.05; to make that clear.
public class Shadow {
static double interestRate = 0.02;
public static void main(String[] args) {
Shadow first = new Shadow();
first.interestRate = 0.05;
System.out.println(Shadow.interestRate);
}
}0.05 0.02 0.02
0.05 0.03 0.03The second line of output shows the problem. When the bank raises the shared rate to 3%, first still uses its own 5%. The safe rule in Python is to read and write class variables through the class name (BankAccount.interest_rate), as every listing here does. Java behaves differently: the compiler accepts first.interestRate = 0.05, but it changes the single static field, and the Java version prints 0.05. Writing the class name in Java too makes it clear to a reader that the change affects every object.
The choice rests on whether each object needs its own value or the whole class needs one value. The comparison table sets out the differences feature by feature.
Instance (non-static) | Static | |
|---|---|---|
Belongs to | One object | The class |
Number of copies | One per object | Exactly one |
Exists | From the object's creation until the object is discarded | From when the class is loaded until the program ends |
Accessed through | An object: | The class name: |
Has | Yes | No |
Typical uses | Data describing one entity (balance, name, position); operations on that data | Counters across all objects, shared settings and constants, utility methods such as |
Static variables have their risks. Because every part of a program can reach a public static variable through the class name, it behaves much like a global variable and brings the same problems: any code can change it, and a bug in one place affects every object. A static variable also cannot hold data about individual objects: a static balance would give every customer the same balance. Instance variables are the default; a member is made static only when it describes the class as a whole.
The class Ticket gives each ticket an identifier from a shared counter; the program after it creates three tickets and copies one reference.
class Ticket:
next_id = 100
def __init__(self, event):
self.event = event
self.id = Ticket.next_id
Ticket.next_id = Ticket.next_id + 5
def describe(self):
return self.event + "-" + str(self.id)
t1 = Ticket("Gala")
t2 = Ticket("Quiz")
t3 = t1
t3.event = "Play"
t4 = Ticket("Gala")
print(t1.describe())
print(t2.describe())
print(t4.describe())
print(Ticket.next_id)In every version of BankAccount so far, any code can write account.balance = -5000.0, and nothing in the class prevents it. A deposit method that checks its amount is pointless if the balance can be changed without calling it. Encapsulation and information hiding are the two principles that close this gap.
Encapsulation concerns the location of code. The rules for changing a balance are written as methods of the account, next to the balance, rather than scattered through the program. Information hiding concerns access to that code and data. The class keeps its data and internal workings out of reach and offers other code only a chosen set of public methods. Encapsulation makes information hiding possible, since data can only be hidden behind methods that sit in the same unit, but a class can be encapsulated without hiding anything, as the earlier versions of BankAccount, with their public attributes, show.
The set of members other code may use is the class's public interface. For an account it is something like: create an account, read the balance, deposit, withdraw, add interest. Everything else, including how and where the balance is stored, is hidden. The capsule diagram shows the arrangement.

Java enforces information hiding with access modifiers, keywords placed before each attribute, method and constructor. A private member can be used only by code inside the same class; a public member can be used by any code. The compiler checks every access, so a program that breaks the rule never runs.
In the listing, owner and balance are private and getBalance is public. The main method in class Bank can read the balance through the method, but its attempt to set the balance directly is refused.
class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
public class Bank {
public static void main(String[] args) {
BankAccount account = new BankAccount("Amira", 120.0);
System.out.println(account.getBalance());
account.balance = -5000.0;
}
}# Python has no access modifiers, so the equivalent assignment is accepted:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def get_balance(self):
return self.balance
account = BankAccount("Amira", 120.0)
print(account.get_balance())
account.balance = -5000.0 # no error: nothing stops it
print(account.get_balance()) # -5000.0Bank.java:19: error: balance has private access in BankAccount
account.balance = -5000.0;
^
1 errorA common design rule makes attributes private and methods public, with a method private only when it is a helper used inside the class. Constructors are normally public so that other code can create objects. In UML, - marks a private member and + a public one.
Python has no private or public keywords, and its interpreter does not stop any code from reading or changing any attribute. Python programmers rely on naming conventions instead. A name starting with one underscore, such as _owner, means "internal: do not use this from outside the class", but nothing enforces it. A name starting with two underscores, such as __balance, triggers name mangling: inside the class it works normally, but Python stores it under the name _BankAccount__balance, so the plain name account.__balance does not reach it from outside.
class BankAccount:
def __init__(self, owner, balance):
self._owner = owner # one underscore: "internal, please don't touch"
self.__balance = balance # two underscores: name is mangled
def get_balance(self):
return self.__balance
account = BankAccount("Amira", 120.0)
print(account.get_balance())
print(account._owner) # works: only a convention
print(account._BankAccount__balance) # works: the mangled name
print(account.__balance) # fails: no attribute with this name// In Java, private is enforced by the compiler; there is no mangled name to get round it.
class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
public class Bank {
public static void main(String[] args) {
BankAccount account = new BankAccount("Amira", 120.0);
System.out.println(account.getBalance()); // 120.0
// account.balance and account.owner do not compile from here
}
}120.0
Amira
120.0
AttributeError: 'BankAccount' object has no attribute '__balance'. Did you mean: 'get_balance'?Name mangling protects against accidents, such as a programmer reusing an attribute name by mistake, but not against a programmer who deliberately uses the mangled name. One further trap follows from it: account.__balance = 0 written outside the class does not change the hidden balance; it quietly creates a separate new attribute with that name. Python's designers chose to trust programmers to respect the conventions, whereas Java's compiler makes the rule impossible to break.
With the attributes hidden, the class must offer methods for anything other code legitimately needs. A method that returns an attribute's value is an accessor; a method that changes an attribute's value is a mutator. By convention their names start with get and set.
Accessors and mutators do not have to come in pairs, and that choice is part of the design. An account's number is fixed when it is opened, so it gets an accessor and no mutator. The balance gets an accessor, but no setBalance; the only ways to change it are deposit, withdraw and addInterest, each of which enforces a rule of the bank.
The value of a mutator is the check it performs. set_owner refuses a blank name; deposit and withdraw refuse amounts that are zero or negative; withdraw refuses to take out more than the balance. This is validation, the input checking of subtopic B2.3, placed inside the class so that it cannot be bypassed. When a value is refused, the method raises an exception (ValueError in Python, IllegalArgumentException in Java), which the calling code can catch and report.
The full class brings together everything built so far: private instance and static variables, a constructor that validates, accessors, mutators with validation, an instance method that uses a static variable, and three static methods.
class BankAccount:
__interest_rate = 0.02 # private class variables
__accounts_opened = 0
def __init__(self, owner, opening_balance=0.0):
if opening_balance < 0:
raise ValueError()
BankAccount.__accounts_opened = BankAccount.__accounts_opened +
.__number = BankAccount.__accounts_opened
.set_owner(owner)
.__balance = opening_balance
():
.__number
():
.__owner
():
.__balance
():
owner.strip() == :
ValueError()
.__owner = owner.strip()
():
BankAccount.is_valid_amount(amount):
ValueError()
.__balance = .__balance + amount
():
BankAccount.is_valid_amount(amount):
ValueError()
amount > .__balance:
ValueError()
.__balance = .__balance - amount
():
.__balance = .__balance + .__balance * BankAccount.__interest_rate
():
+ (.__number) + + .__owner + + (.__balance)
():
BankAccount.__accounts_opened
():
rate < rate > :
ValueError()
BankAccount.__interest_rate = rate
():
amount >
account = BankAccount(, )
account.deposit()
account.withdraw()
(account)
:
account.withdraw()
ValueError error:
(, error)
:
account.set_owner()
ValueError error:
(, error)
(account.get_owner(), account.get_balance())
(BankAccount.get_accounts_opened())#1 Amira: 220.0
Refused: insufficient funds
Refused: owner name cannot be empty
Amira 220.0
1The constructor calls set_owner rather than assigning the name directly, so the blank-name rule is written once and applies both when an account is opened and when its owner changes. The extra spaces around " Amira " were removed by the same method. Both refused operations left the object exactly as it was: the balance is still 220.0 and the owner is still Amira.
A gym's booking program stores each member's name and the number of class credits they have left. The first version of its class leaves both attributes public, and the program that uses it subtracts credits directly.
class GymMember:
def __init__(self, name, credits):
self.name = name
self.credits = credits
member = GymMember("Lena", 3)
member.credits = member.credits - 5
print(member.credits)class GymMember {
String name;
int credits;
GymMember(String name, int credits) {
this.name = name;
this.credits = credits;
}
}
public class GymOld {
public static void main(String[] args) {
GymMember member = new GymMember("Lena", 3);
member.credits = member.credits - 5;
System.out.println(member.credits);
}
}Idiomatic Python often replaces get_ and set_ methods with properties. The @property decorator turns a method into something that is read like an attribute, and a matching @name.setter method runs whenever the attribute is assigned. Callers write the simple form account.owner = "Amira Haddad", yet the mutator's validation still runs. A property with no setter is read-only.
class BankAccount:
def __init__(self, owner, balance=0.0):
self.__balance = balance
self.owner = owner # runs the setter below
@property
def balance(self): # accessor: read with account.balance
return self.__balance
@property
def owner(self):
return self.__owner
@owner.setter
def owner(self, value): # mutator: runs on account.owner = ...
if value.strip() == "":
raise ValueError("owner name cannot be empty")
self.__owner = value.strip()
account = BankAccount("Amira", 120.0)
account.owner =
(account.owner, account.balance)
:
account.balance =
AttributeError error:
(, error)// Java has no properties: the same design uses an accessor and a validating mutator,
// and simply provides no setBalance method.
class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
this.balance = balance;
setOwner(owner);
}
public double getBalance() {
return balance;
}
public String getOwner() {
return owner;
}
public void setOwner(String value) {
if (value.trim().isEmpty()) {
throw new IllegalArgumentException();
}
owner = value.trim();
}
}
{
{
(, );
account.setOwner();
System.out.println(account.getOwner() + + account.getBalance());
}
}Amira Haddad 120.0
Refused: property 'balance' of 'BankAccount' object has no setterProperties are a Python feature only. Plain get_balance() and set_owner() methods are equally correct and match the Java version line for line.
Hiding an object's data costs extra code, so the benefits need to be clear. Four main benefits justify the cost.
Integrity. An object's data can only change through methods that enforce its rules, so every object stays in a valid state for its whole life: no account can have a negative balance or a blank owner. A whole family of bugs, in which a distant line of code quietly corrupts the data, is ruled out.
Security. Sensitive data, such as a balance or a password, can be read or changed only in the ways the class allows. Read-only data gets no mutator; some data, such as a stored password, gets no accessor at all, only a method that checks a guess against it. Access modifiers are a design tool, not encryption: they protect objects from other parts of the program, not from someone with access to the computer's memory.
Freedom to change the implementation. Code outside the class depends only on the public interface. The class can change how it stores its data, and as long as its public methods behave in the same way, no other code needs to change.
Easier debugging and teamwork. If a balance is wrong, the error must be in one of the few methods that can change it, not anywhere in a program of thousands of lines. Programmers in a team can use a class from its public interface alone, without reading its code.
The third benefit can be shown with a real problem. A floating-point double cannot store 0.1 exactly, so adding 0.1 ten times gives 0.9999999999999999 instead of 1.0, the rounding drift described in subtopic B2.1, Programming fundamentals. A bank would rather store whole cents in an integer. Because balance is private, the class can switch to cents without any change to the code that uses it.
class BankAccount:
def __init__(self, owner, opening_balance=0.0):
self.__owner = owner
self.__cents = round(opening_balance * 100) # now stored as whole cents
def get_balance(self):
return self.__cents / 100
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self.__cents = self.__cents + round(amount * 100)
account = BankAccount("Kenji", 0.0)
for i in range(10):
account.deposit(0.1)
print(account.get_balance())
total = 0.0
for i in ():
total = total +
(total)class BankAccount {
private String owner;
private long cents; // now stored as whole cents
public BankAccount(String owner, double openingBalance) {
this.owner = owner;
this.cents = Math.round(openingBalance * 100);
}
public double getBalance() {
return cents / 100.0;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("deposit must be positive");
}
cents = cents + Math.round(amount * );
}
}
{
{
(, );
( ; i < ; i++) {
account.deposit();
}
System.out.println(account.getBalance());
;
( ; i < ; i++) {
total = total + ;
}
System.out.println(total);
}
}1.0
0.9999999999999999The calls deposit(0.1) and get_balance() are exactly as before, so every program that used the old class still works, and now gets exact answers. Had other code been allowed to write account.balance = … directly, every one of those lines would have had to be found and rewritten.
Making an attribute private is not enough when the attribute refers to a mutable object such as a list. An accessor that returns the list itself returns a reference to it, and the caller can then change the private list through that reference without calling any method of the class. The safe accessor returns a copy.
class BankAccount:
def __init__(self, owner):
self.__owner = owner
self.__balance = 0.0
self.__history = []
def deposit(self, amount):
self.__balance = self.__balance + amount
self.__history.append("deposit " + str(amount))
def get_history_unsafe(self):
return self.__history # hands out the private list itself
def get_history(self):
return list(self.__history) # hands out a copy
account = BankAccount("Amira")
account.deposit(40.0)
account.deposit(15.0)
copy = account.get_history()
copy.clear()
print((account.get_history()))
leaked = account.get_history_unsafe()
leaked.clear()
((account.get_history()))import java.util.ArrayList;
class BankAccount {
private String owner;
private double balance;
private ArrayList<String> history;
public BankAccount(String owner) {
this.owner = owner;
this.balance = 0.0;
this.history = new ArrayList<String>();
}
public void deposit(double amount) {
balance = balance + amount;
history.add("deposit " + amount);
}
public ArrayList<String> getHistoryUnsafe() {
return history; // hands out the private list itself
}
ArrayList<String> {
<String>(history);
}
}
{
{
();
account.deposit();
account.deposit();
ArrayList<String> copy = account.getHistory();
copy.clear();
System.out.println(account.getHistory().size());
ArrayList<String> leaked = account.getHistoryUnsafe();
leaked.clear();
System.out.println(account.getHistory().size());
}
}2
0Clearing the copy leaves the account's two history entries intact; clearing the leaked list wipes them, even though history is private. Strings and numbers cannot be changed in place, so returning them is always safe; lists, ArrayLists and other objects need the copy.
Before a class is written, it is designed. The Unified Modeling Language (UML) is a standard set of diagrams for describing software, and its class diagram is the standard way to show a class's design on paper, independent of the programming language that will implement it.
Each class is drawn as a box with three compartments. The top holds the class name in bold. The middle lists the attributes, one per line, as visibility name : Type, optionally followed by = initial value. The bottom lists the methods as visibility name(parameter : Type, …) : ReturnType; a constructor has the class's name and no return type, and a method that returns nothing has void (or no return type).
Visibility is + for public and - for private, and static members are underlined. UML also has # for protected and ~ for default (package) access, which matter only once classes inherit from each other (subtopic B3.2, HL only). The UML class diagram of the final BankAccount class follows these rules.

UML is language-neutral, so the diagram uses Java-style names and types (double, String, camelCase). A Python implementation keeps the same design with Python names (get_balance, opening_balance) and without declared types; UML types such as double become Python float. A diagram does not have to show every detail. This one leaves out the Java version's second, one-argument constructor, and designers often omit accessors and mutators that simply read or set one attribute, or show only the members relevant to the task in hand. What a diagram does show must match the code exactly.
YouTubeA design starts from the requirements, the description of what the program must do. A practical method reads the description three times.
Find the classes. Underline the nouns. Things the system must remember several facts about, and of which there may be many, become classes (customer, account). Nouns that are just a single value become attributes instead (name, balance).
Find the attributes. For each class, list the data that each object must hold, with a type for each. Decide which values each object holds separately (instance) and which are shared by the class (static).
Find the methods. Circle the verbs. Actions the system performs on a thing become methods of its class (deposit, withdraw). Give each method its parameters and return type, and add a constructor that sets the initial state.
The method then checks the design against the requirements: every requirement should be met by some method, every attribute should be needed by some method, and every attribute should be private unless there is a strong reason otherwise. A design is rarely right the first time; this checking step often moves an attribute to another class or splits a large class in two.
Few programs have only one class. When objects of one class need to use or keep track of objects of another, the two classes are related, and the class diagram shows the relationship as a line between them. The simplest relationship is an association.
Numbers at each end of the line say how many objects can take part. Each customer holds no accounts, one or several; each account belongs to exactly one customer.
The class diagram reads in both directions: from Customer, "one customer holds 0..* accounts"; from BankAccount, "each account is held by 1 customer". A multiplicity is written next to the class it counts.

In code, an association usually becomes an attribute that holds references. A Customer object keeps a list of references to its BankAccount objects, and its methods loop over them. The list is private, so accounts can only be added through open_account, and the total is calculated with a hand-written loop.
class BankAccount:
def __init__(self, owner, balance=0.0):
self.__owner = owner
self.__balance = balance
def get_balance(self):
return self.__balance
class Customer:
def __init__(self, name):
self.__name = name
self.__accounts = [] # 0..* references to BankAccount objects
def open_account(self, amount):
account = BankAccount(self.__name, amount)
self.__accounts.append(account)
return account
def count_accounts(self):
return len(self.__accounts)
():
total =
account .__accounts:
total = total + account.get_balance()
total
kenji = Customer()
kenji.open_account()
kenji.open_account()
(kenji.count_accounts(), kenji.total_balance())import java.util.ArrayList;
class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
class Customer {
private String name;
private ArrayList<BankAccount> accounts; // 0..* references to BankAccount objects
public Customer(String name) {
this.name = name;
.accounts = <BankAccount>();
}
BankAccount {
(name, amount);
accounts.add(account);
account;
}
{
accounts.size();
}
{
;
(BankAccount account : accounts) {
total = total + account.getBalance();
}
total;
}
}
{
{
();
kenji.openAccount();
kenji.openAccount();
System.out.println(kenji.countAccounts() + + kenji.totalBalance());
}
}2 345.5total_balance asks each account for its balance through its public accessor; it never touches another class's private data. In UML the list of accounts is shown by the association line rather than repeated as an attribute in Customer. HL students meet the stronger forms of this relationship, aggregation and composition, and inheritance between classes, in subtopic B3.2.
Banks offer several kinds of account. A savings account is a bank account that also earns bonus interest and limits withdrawals; a current account is a bank account that also allows an overdraft. Writing each as a separate class would repeat every shared attribute and method in each one, and a fix to deposit would then have to be made in every copy.
The existing class is the superclass (parent) and the new one the subclass (child). The relationship reads as "is a": a savings account is a bank account, so anything that works on bank accounts also works on savings accounts. Inheritance is the right tool only when this sentence is true; a customer is not a bank account, so Customer must not inherit from BankAccount.
The strengths of inheritance are reuse and consistency: shared code is written, tested and fixed once in the superclass, and a new kind of account needs only the code that makes it different. Its weaknesses appear as hierarchies grow. A subclass depends closely on the details of its superclass, so a change to the superclass can break subclasses written by other people, a problem known as the fragile base class. Deep hierarchies, with classes inheriting from classes that inherit from others, are hard to follow because the code for one object is spread over many files. In Java a class can inherit from only one superclass (Python allows several), and in practice many designers prefer to build objects from other objects rather than inherit when the "is a" test is in doubt.
The word means "many forms". In programming, polymorphism lets one operation name be used with objects of different types, each responding in its own way. Familiar operators already behave like this. The + operator adds two integers but joins two strings, and print (System.out.println in Java) displays a number, a string or a whole list, each in a suitable form.
balances = [120.0, 45.5]
print(3 + 4) # + adds two integers
print("3" + "4") # + joins two strings
print(balances) # print displays a list in its own wayimport java.util.ArrayList;
public class Poly {
public static void main(String[] args) {
ArrayList<Double> balances = new ArrayList<Double>();
balances.add(120.0);
balances.add(45.5);
System.out.println(3 + 4); // + adds two integers
System.out.println("3" + "4"); // + joins two strings
System.out.println(balances); // println displays a list in its own way
}
}7
34
[120.0, 45.5]With classes, polymorphism usually works together with inheritance. Suppose SavingsAccount and CurrentAccount both inherit addInterest() from BankAccount, and each replaces it with its own version (savings add a bonus; current accounts with an overdraft are charged instead). A program can keep all accounts in one list and call addInterest() on each; every object runs the version belonging to its own class. The end-of-month loop never needs an if that checks the kind of account, and a new kind of account added next year works with the loop unchanged. The __str__ and toString() methods already written for BankAccount are a small example of the same idea: print calls the same method name on every object, and each class supplies its own version. Writing subclasses that replace inherited methods (method overriding) is HL content, covered with code in subtopic B3.2.
Concept | What it means | Main strength | Main limitation |
|---|---|---|---|
Class and object | One description, many objects with their own state | Models real entities naturally | Extra structure for small problems |
Encapsulation | Data and its methods bundled together, details hidden | Integrity; implementation can change safely | More code (accessors, mutators) |
Inheritance | New class built from an existing one ("is a") | Reuse; consistent shared behaviour | Tight dependence; deep hierarchies fragile |
Polymorphism | One method name, behaviour chosen by the object's class | Code extends to new types without changes | Harder to trace which version runs |
OOP's main advantage is that the structure of the program matches the structure of the problem. People already think about the world as things with properties and behaviours, so a design that has a Customer, an Account and a Transaction is easy to discuss with the bank that asked for the software. The same structure divides the work: separate classes can be written, tested and maintained by different programmers who agree only on the public interfaces, which supports modularisation in large teams.
Encapsulation protects each object's data, so errors are caught where they happen and found quickly, and classes can be improved inside without breaking the rest of the program. A tested class can be reused in later programs, and libraries such as Java's ArrayList or Python's list are themselves classes reused by millions of programs. Inheritance adds reuse between related classes, and polymorphism lets a program be extended with new types without rewriting code that already works.
Not every program needs these benefits, which is why the question of whether OOP is necessary for all programming, or only for modelling complex situations, comes up whenever a paradigm is chosen.
The same structure has costs. For a short task, such as a script that reads a file of marks and prints their average, defining classes adds lines and planning without making the solution clearer: a few procedures do the job. OOP also takes longer to learn, because a beginner must understand references, constructors, self references and static members before writing a useful class, and a poor design (classes that know too much about each other, or deep inheritance) can make a program harder to change than a procedural one.
There is a performance cost as well. Each object carries some memory overhead beyond its data, and calling a method is slightly slower than reading a variable. In most applications the difference is too small to matter, but in programs that process millions of simple records, such as large numeric calculations or data analysis, programmers often prefer plain arrays and functions. Some problems also fit other paradigms better: a pipeline that transforms data step by step is naturally a sequence of functions, and a database query is naturally written in SQL.
Scenario | Fit for OOP | Reasoning |
|---|---|---|
Video game with many enemies, players and items | Strong | Each entity is naturally an object with its own state (position, health) and behaviour; polymorphism lets each kind of enemy move differently |
Banking or school management system | Strong | Real entities with rules about their data; encapsulation enforces the rules; large team and long life |
Graphical user interface | Strong | Buttons, windows and menus are objects with properties and event-handling methods |
One-off script to rename a folder of files | Weak | A few lines of procedural code; classes add structure without benefit |
Numeric simulation over millions of values | Mixed | Arrays of numbers are faster and use less memory than millions of small objects |
A student's IB internal assessment solution | Usually strong | Several kinds of data with rules, built and tested in parts over months |
Object-oriented programming began in Norway. Ole-Johan Dahl and Kristen Nygaard developed two languages, Simula I and Simula 67, at the Norwegian Computing Center in Oslo in the 1960s. The first version, ready by 1962, was designed for discrete event simulation: programs that model systems such as queues of customers, where things arrive, wait, are served and leave. Simula 67 turned the same ideas into a general-purpose language and is considered the first object-oriented programming language.
Date | Event |
|---|---|
1962 | First Simula version, designed for simulations |
January 1965 | Simula I fully operational on the Norwegian Computing Center's UNIVAC 1107 |
May 1967 | Dahl and Nygaard present their paper on class and subclass declarations in Oslo, the first formal definition of Simula 67 |
February 1968 | Simula 67 formally standardised |
November 2001 | IEEE John von Neumann Medal "for the introduction of the concepts underlying object-oriented programming" |
April 2002 | ACM 2001 Turing Award "for ideas fundamental to the emergence of object oriented programming" |
Source: Wikipedia, “Simula”, citing the ACM and IEEE award citations; accessed October 2026.
Why simulation led to objects. A simulation of a shop has many customers, each at a different stage, each with their own data (when they arrived, what they want), all following the same rules. This is exactly the situation that classes and objects describe: one Person class for the rules, many person objects for the individuals. Simula 67 introduced objects, classes, subclasses with inheritance and virtual procedures (a subclass's own version of a procedure, the mechanism behind polymorphism), and it collected the memory of unused objects automatically, as Java and Python do today. A well-known example Simula program models a shop's fitting room as an object with a queue at its door, and each shopper as an object that browses, waits for the fitting room and leaves.
In a Simula shop simulation | In OOP terms |
|---|---|
The rules every shopper follows | A class ( |
Each individual shopper | An object, an instance of |
A shopper's arrival time and current activity | The object's state, held in attributes |
Browsing, waiting, leaving | Methods |
A shopper as a special kind of simulated process | Inheritance: |
Influence. Bjarne Stroustrup, who created C++, and James Gosling, who designed Java (released by Sun Microsystems in May 1995), have both acknowledged Simula as a major influence. Simula's class model passed through C++ into Java, and Python (first released by Guido van Rossum in 1991) offers classes in the same tradition. Java made the class compulsory for all code; Python made it optional.
What it shows. The case shows that OOP was invented to model real-world entities, the starting point of every class design, and that its first strength was in programs where many similar things each have their own state, which is still where it fits best. It also shows the evaluation in practice: Simula itself was built for one specialised kind of program, yet its ideas spread to almost every mainstream language once C++ and Java carried them into general use.
Object-oriented programming for a single class is examined in Paper 2, in the student's own language (Python or Java). Expect a question built around one class in a context: Construct a UML class diagram from a description (4–6 marks), construct a constructor, accessor, mutator or other method (3–5 marks), construct code that creates objects and calls their methods, distinguish between static and instance members of the given class (2–4 marks), explain why an attribute should be private or how encapsulation protects the object (3–4 marks), and evaluate the use of OOP for a scenario (4–6 marks). Code marks go to the features of the solution (correct attribute use, the validation, the return value), not to perfect syntax.
When a question gives you a UML diagram and asks for code, copy the names exactly (seatsSold, not seats) and keep the visibility: a private attribute in the diagram is private in your code, and in Python is written with a leading double underscore. Use self. (Python) on every attribute and parameter assignment, and never give a Java constructor a return type: public void Screening(...) is an ordinary method, not a constructor, and the class then has no constructor that accepts a title.
State the purpose of a constructor. [1]
Outline the difference between a class and an object, using a Car class as an example. [2]
Identify two features of the line - total : double in a UML class diagram, and state how a static attribute is shown. [3]
Python code: a = Locker(5), then b = a, then b.assign("Mia"). Explain why a.is_free() now returns False. [2]
Distinguish between an instance method and a static method. [2]
A Java class contains void Player(String name) { this.name = name; } and the program creates new Player("Ana"). Explain why this does not compile, and how to fix it. [2]
Construct a class Thermostat (Python or Java) with a private attribute target, a constructor setting it to 20, an accessor, and a mutator that accepts only values from 5 to 30 inclusive and returns whether the change was made. [5]
A Python class has count = 0 in its class body. A method contains self.count = self.count + 1. Explain why the class's shared count does not change. [2]
Explain two benefits of information hiding for a class that stores a user's password. [4]
Evaluate the use of OOP to write a multiplayer quiz game for a school's open day. [6]
To set the initial values of a new object's attributes when it is created (giving it a valid starting state).
A class is the template that defines the attributes and methods every car has (e.g. make, speed, accelerate()) [1]; an object is one particular car made from it with its own values (e.g. a red Toyota at 50 km/h) [1].
- shows the attribute is private [1]; : double gives its data type, total its name [1]. A static attribute is underlined [1].
b = a copies the reference, not the locker [1]; a and b refer to the same object, so the assignment through b changed the owner of the locker that a refers to [1].
An instance method is called on an object and can use that object's instance variables through self/this [1]; a static method is called on the class, has no self reference and can use only parameters and static members [1].
A return type (void) makes it an ordinary method, so the class has only the default no-argument constructor and new Player("Ana") matches no constructor [1]; remove void: Player(String name) { this.name = name; } [1].
Award [1] each: private attribute; constructor sets target to 20; accessor returns it; mutator tests 5 <= value <= 30 (both ends included); assigns and returns true only when valid, otherwise returns false. A model answer follows the list.
Reading self.count finds the class attribute (0), but assigning to self.count creates a new instance variable on that one object [1]; the class attribute is never updated, so it must be written ClassName.count = ClassName.count + 1 [1].
Any two, each with a consequence [2 each]: the password attribute is private, so other code cannot read it and it cannot be leaked or printed by mistake; there is no accessor, only a method that checks a guess, so the password is never handed out; changes go only through a mutator that enforces password rules (length, characters), so no weak password can be stored; the storage can later change (e.g. to a hash) without changing other code.
Strengths: players, questions and rounds map naturally to classes with their own state; several students can build separate classes in parallel; encapsulation keeps scores valid (only
OOP is a programming paradigm that builds programs from objects, each combining data (attributes) with behaviour (methods); a class is the template, an object (instance) is one entity with its own state.
A constructor (__init__ in Python; the class name with no return type in Java) runs at instantiation and sets the object's initial state; Java constructors can be overloaded, Python uses default parameters.
self (Python) and this (Java) refer to the current object; this.x = x distinguishes an attribute from a parameter of the same name.
Object variables hold references: assigning one to another shares one object, and None/null refers to no object.
Instance variables and methods belong to each object; static variables are one shared copy in the class, and static methods are called on the class and cannot use instance data.
Encapsulation bundles data with its methods; information hiding makes attributes private and exposes a public interface of accessors and validating mutators.
Java enforces private and public in the compiler; Python relies on the _ and __ conventions (name mangling) and offers @property.
A UML class diagram shows name, attributes and methods with visibility (+, -), types, parameters and return types; static members are underlined; associations carry multiplicities.
Inheritance reuses a class's members in a new "is a" class; polymorphism lets one method name behave differently for different classes; their code is HL (B3.2).
OOP suits large, long-lived programs with many interacting entities; it adds code, learning effort and some overhead for small or data-processing tasks.
I can explain the difference between a class, an object and an instance with my own example.
I can write a class in Python or Java with private attributes, a constructor and methods, and create several objects from it.
I can explain why this.owner = owner and self.owner = owner need the self reference.
I can give a Java class two constructors and give the Python version a default parameter instead.
I can write __str__ or toString() and store objects in a list, then loop over them without built-in functions.
I can trace code in which two variables refer to the same object, and explain what None or null means.
I can distinguish between static and instance variables and methods, and choose which to use for a given attribute.
I can explain why a static method cannot use instance variables, tell Python's @staticmethod from @classmethod, and avoid Python's self.count trap.
I can write accessors and mutators that validate, and explain why a mutator without validation adds no protection.
I can explain how Java enforces private and how Python's _ and __ conventions differ from it.
155 exam-style questions on this subtopic, written in the style of the IB papers. The first has its markscheme open. Counted on
A school library lets students reserve books that are on loan. The reserved titles are held in a singly linked list. The list class is shown below.
class ReservationList:
def __init__(self):
self.head = Nonepublic class ReservationList {
private Node head;
public ReservationList() {
head = null;
}
}Each node holds a book title (a string) and a reference to the next node.
Construct the class Node, including a constructor that takes a title.
3 marks
Award [1] per point, up to [3 max].
class header Node;
an attribute for the title (string);
an attribute for the next reference;
constructor that takes the title and assigns it;
next initialised to null / None;
Example answer (Python):
class Node:
def __init__(self, title):
self.title = title
self.next = NoneExample answer (Java):
public class Node {
String title;
Node next;
public Node(String title) {
this.title = title;
this.next = null;
}
}Construct the method add_to_front (Python) / addToFront (Java) that adds a new title at the start of the list.
3 marks
Award [1] per point, up to [3 max].
creates a new node holding the title;
sets the new node's next to the current head;
sets head to the new node;
the two assignments are in the correct order (next set before head changed);
Note: The method works for an empty list without a special case; do not penalise a correct special case.
Example answer (Python):
def add_to_front(self, title):
new_node = Node(title)
new_node.next = self.head
self.head = new_nodeExample answer (Java):
public void addToFront(String title) {
Node newNode = new Node(title);
newNode.next = head;
head = newNode;
}Construct the method display that outputs every title in the list, from the first to the last.
3 marks
Award [1] per point, up to [3 max].
a temporary reference starts at head (head itself is not moved);
loop that continues while the current reference is not null;
outputs the title of the current node;
moves to the next node inside the loop;
Example answer (Python):
def display(self):
curr = self.head
while curr is not None:
print(curr.title)
curr = curr.nextExample answer (Java):
public void display() {
Node curr = head;
while (curr != null) {
System.out.println(curr.title);
curr = curr.next;
}
}Construct the method contains that takes a title and returns true if that title is in the list and false otherwise. For example, if the list holds "Dune", "Emma" and "Holes", contains("Emma") returns true and contains("Matilda") returns false.
4 marks
Award [1] per point, up to [4 max].
traversal from head using a temporary reference;
correct stopping condition (null reached);
compares the title in each node with the parameter;
returns true as soon as a match is found;
returns false only after the whole list has been checked;
Note: Accept == for string comparison in Java. Accept a flag variable instead of an early return.
Example answer (Python):
def contains(self, title):
curr = self.head
while curr is not None:
if curr.title == title:
return True
curr = curr.next
return FalseExample answer (Java):
public boolean contains(String title) {
head;
(curr != ) {
(curr.title.equals(title)) {
;
}
curr = curr.next;
}
;
}Explain why the library does not need to decide in advance how many reservations the list can hold.
2 marks
Award [1] per point, up to [2 max].
A linked list is a dynamic data structure;
memory for each node is allocated only when a reservation is added;
nodes do not have to be in a contiguous block, so the list can keep growing while memory is available / no fixed size has to be declared as for an array;
memory from removed nodes can be reclaimed;
A photo-slideshow app lets a user step forwards and backwards through their photos and drop new photos in anywhere. The photos are stored in a doubly linked list.
class Node:
def __init__(self, photo):
self.photo = photo
self.prev = None
self.next = None
class Slideshow:
def __init__(self):
self.head = None
self.tail = Nonepublic class Node {
String photo;
Node prev;
Node next;
public Node(String photo) {
this.photo = photo;
this.prev = null;
this.next = ;
}
}
{
Node head;
Node tail;
}A lightweight web browser for a smart TV remembers the pages a user has visited so that the Back button can return to them. It uses a stack built on a fixed-size array. top holds the index of the item on top of the stack.
class HistoryStack:
def __init__(self, capacity):
self.pages = [None] * capacity
self.top = -1
def is_empty(self):
return self.top == -1
def is_full(self):
return self.top == len(self.pages) - 1
def push(self, url):
# code missing
def pop(self):
# code missingOne of 65 short multiple-choice questions on B3.1. Pick an answer to see why it's right or wrong.
Inside a Java instance method, what does refer to?
The official deck for this part of the syllabus.
What is a programming paradigm?
What is procedural programming?
Java:
import java.util.ArrayList;
class Locker {
private int number;
private String owner; // null means the locker is free
public Locker(int number) {
this.number = number;
this.owner = null;
}
public int getNumber() {
return number;
}
public boolean isFree() {
return owner == null;
}
public boolean assign(String name) {
if (!isFree() || name.trim().isEmpty()) {
return false;
}
owner = name.trim();
return true;
}
public void release() {
owner = null;
}
}
public class Lockers {
public static void main(String[] args) {
ArrayList<Locker> lockers = new ArrayList<Locker>();
for (int n = 1; n <= 3; n++) {
lockers.add(new Locker(n));
}
System.out.println(lockers.get(0).assign("Priya") + " " + lockers.get(0).assign("Tom") + " " + lockers.get(1).assign(""));
int freeCount = 0;
for (Locker locker : lockers) {
if (locker.isFree()) {
freeCount = freeCount + 1;
}
}
System.out.println(freeCount);
}
}Output (Python prints True False False, Java true false false):
True False False
2Priya's request succeeds; Tom's is refused because locker 1 is taken; the blank name is refused; lockers 2 and 3 are still free, so the count is 2. Marks in (a) go to the private attributes, the constructor setting the owner to None/null, is_free returning a Boolean from the owner, both refusal conditions in assign, and release resetting the owner.
@staticmethod
def is_valid_isbn(code):
if len(code) != 13:
return False
for ch in code:
if ch < "0" or ch > "9":
return False
return TrueJava:
public static boolean isValidIsbn(String code) {
if (code.length() != 13) {
return false;
}
for (int i = 0; i < code.length(); i++) {
char ch = code.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
return true;
}Check: "9780198307662" gives true; "97801983076X2" gives false (the X); "978019830766" gives false (only 12 characters). Marks go to the length test, the loop over every character with the digit comparison, and returning false on failure and true only at the end.
Each constructor call takes the current counter as the new ticket's id, then adds 5 to the shared counter. t3 = t1 creates no object: t3 and t1 refer to the same ticket, so changing t3.event changes the event that t1.describe() reports.
Statement |
| Object A | Object B | Object C |
|---|---|---|---|---|
start | 100 | |||
| 105 | Gala, 100 (t1) | ||
| 110 | Gala, 100 (t1) | Quiz, 105 (t2) | |
| 110 | Gala, 100 (t1, t3) | Quiz, 105 (t2) | |
| 110 | Play, 100 (t1, t3) | Quiz, 105 (t2) | |
| 115 | Play, 100 (t1, t3) | Quiz, 105 (t2) | Gala, 110 (t4) |
Output, confirmed by running both versions:
Play-100
Quiz-105
Gala-110
115The common wrong answer is Gala-100 on the first line, from treating t3 = t1 as a copy of the ticket.
class BankAccount {
private static double interestRate = 0.02; // private class variables
private static int accountsOpened = 0;
private int number; // private instance variables
private String owner;
private double balance;
public BankAccount(String owner, double openingBalance) {
if (openingBalance < 0) {
throw new IllegalArgumentException("opening balance cannot be negative");
}
accountsOpened = accountsOpened + 1;
this.number = accountsOpened;
setOwner(owner); // reuse the mutator's validation
this.balance = openingBalance;
}
public BankAccount(String owner) {
this(owner, 0.0);
}
// accessors
public int getNumber() {
return number;
}
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
// mutators
public void setOwner(String owner) {
if (owner.trim().isEmpty()) {
throw new IllegalArgumentException("owner name cannot be empty");
}
this.owner = owner.trim();
}
public void deposit(double amount) {
if (!isValidAmount(amount)) {
throw new IllegalArgumentException("deposit must be positive");
}
balance = balance + amount;
}
public void withdraw(double amount) {
if (!isValidAmount(amount)) {
throw new IllegalArgumentException("withdrawal must be positive");
}
if (amount > balance) {
throw new IllegalArgumentException("insufficient funds");
}
balance = balance - amount;
}
public void addInterest() {
balance = balance + balance * interestRate;
}
public String toString() {
return "#" + number + " " + owner + ": " + balance;
}
// static members
public static int getAccountsOpened() {
return accountsOpened;
}
public static void setInterestRate(double rate) {
if (rate < 0 || rate > 0.1) {
throw new IllegalArgumentException("rate must be between 0 and 0.1");
}
interestRate = rate;
}
public static boolean isValidAmount(double amount) {
return amount > 0;
}
}
public class Bank {
public static void main(String[] args) {
BankAccount account = new BankAccount(" Amira ", 200.0);
account.deposit(50.0);
account.withdraw(30.0);
System.out.println(account);
try {
account.withdraw(500.0);
} catch (IllegalArgumentException error) {
System.out.println("Refused: " + error.getMessage());
}
try {
account.setOwner(" ");
} catch (IllegalArgumentException error) {
System.out.println("Refused: " + error.getMessage());
}
System.out.println(account.getOwner() + " " + account.getBalance());
System.out.println(BankAccount.getAccountsOpened());
}
}Java:
class GymMember {
private String name;
private int credits;
public GymMember(String name, int credits) {
if (credits < 0) {
throw new IllegalArgumentException("credits cannot be negative");
}
this.name = name;
this.credits = credits;
}
public String getName() {
return name;
}
public int getCredits() {
return credits;
}
public boolean bookClass() {
if (credits == 0) {
return false;
}
credits = credits - 1;
return true;
}
public boolean topUp(int n) {
if (n < 1 || n > 50) {
return false;
}
credits = credits + n;
return true;
}
}Marks go to: private attributes [1]; the constructor's negative check [1]; both accessors [1]; bookClass refusing at zero and subtracting one otherwise [1]; topUp accepting only 1 to 50 [1]; correct Boolean return values in both methods [1].
(c) The first booking uses Lena's only credit (true); the second is refused (false); a top-up of 60 is outside 1 to 50 (false); a top-up of 10 succeeds (true), leaving 10 credits. Running the class gives (Java prints the Booleans in lower case):
True False False True
Lena 10The statement member.credits = member.credits - 5 from the old program now fails: Java refuses to compile it, and Python stops with AttributeError, because the value is stored under the mangled name and member.credits no longer exists.
Marks would typically go to: two classes with correct names; appropriate private attributes with types; methods with parameters and return types; the static attribute underlined; the association line; correct multiplicities at both ends.
The judgement in (c) must follow from the scenario: a general statement that inheritance "promotes reuse" without asking whether there is enough different behaviour to reuse reaches only the lower marks.
(b) Award [1] for each, up to [3]:
parameters for title and capacity assigned to the attributes using this. / self.;
seats sold set to 0;
the static counter increased by 1 (through the class name in Python).
def __init__(self, title, capacity):
self.__title = title
self.__capacity = capacity
self.__seats_sold = 0
Screening.__screenings_created = Screening.__screenings_created + 1Java:
public Screening(String title, int capacity) {
this.title = title;
this.capacity = capacity;
this.seatsSold = 0;
screeningsCreated = screeningsCreated + 1;
}(c) Award [1] for each, up to [4]:
tests n <= 0 and refuses;
tests n against the seats left (capacity − seats sold) and refuses if larger;
adds n to seats sold only when valid;
returns true/True when sold and false/False otherwise.
def sell_seats(self, n):
if n <= 0 or n > self.seats_left():
return False
self.__seats_sold = self.__seats_sold + n
return TrueJava:
public boolean sellSeats(int n) {
if (n <= 0 || n > seatsLeft()) {
return false;
}
seatsSold = seatsSold + n;
return true;
}(For a screening with capacity 120, selling 118 then 3 then 2 returns true, false, true and leaves 0 seats.)
(d) Award [1] for each side of the contrast:
the counter is static: one copy belongs to the class and is shared by (updated for) all screenings;
capacity is an instance variable: each screening object stores its own value, which can differ between screens.
(e) Award [1] for each linked point, up to [3]:
other classes cannot change the value directly / only methods of Screening can change it;
all changes go through sellSeats, which validates the number of seats;
so the screening can never be oversold / the seats sold can never be negative or exceed capacity (integrity of the object's state);
OR
the way sales are stored can change later without affecting other classes.
Total [16]
addPointsModel answer for question 7 (the test set_target(30), set_target(31), set_target(4) returns true, false, false and leaves the target at 30):
class Thermostat:
def __init__(self):
self.__target = 20
def get_target(self):
return self.__target
def set_target(self, value):
if value < 5 or value > 30:
return False
self.__target = value
return TrueJava:
class Thermostat {
private int target;
public Thermostat() {
target = 20;
}
public int getTarget() {
return target;
}
public boolean setTarget(int value) {
if (value < 5 || value > 30) {
return false;
}
target = value;
return true;
}
}I can explain the benefits of information hiding: integrity, security, changing the implementation, and debugging.
I can construct a UML class diagram from a description, with visibility, types, parameters, return types and underlined static members.
I can draw an association between two classes with correct multiplicities and turn it into code with a list of references.
I can explain inheritance and polymorphism with an example, without writing their code.
I can evaluate the use of OOP for a given scenario and reach a justified judgement.
The slideshow holds the photos beach.jpg, cliff.jpg and dunes.jpg, in that order.
Sketch the doubly linked list, showing head, tail and every reference.
3 marks
Construct the method insert_after (Python) / insertAfter (Java) that takes a reference node to a node already in the list and a photo name, and inserts a new node holding that photo immediately after node. Your method must work when node is the last node.
6 marks
Explain the effect on the app if the method in part (b) did not update the prev reference of the node that follows the new node.
2 marks
State the time complexity of insert_after / insertAfter, and state the time complexity of inserting a photo after the photo with a given name. Assume the slideshow holds n photos.
2 marks
class HistoryStack {
private String[] pages;
private int top;
public HistoryStack(int capacity) {
pages = new String[capacity];
top = -1;
}
public boolean isEmpty() { return top == -1; }
public boolean isFull() { return top == pages.length - 1; }
public boolean push(String url) {
// code missing
}
public String pop() {
// code missing
}
}State the value of top when the stack holds 4 pages.
1 mark
Construct the push method. If the stack is full it must return False / false and leave the stack unchanged; otherwise it adds url to the top and returns True / true.
3 marks
Construct the pop method. It returns the URL on top of the stack and removes it, or returns None / null if the stack is empty.
3 marks
A HistoryStack is created with a capacity of 3. The following operations are then carried out in order:
push("home"), push("news"), push("sport"), push("weather"), pop(), push("games"), pop(), pop()
Assuming push and pop work as described, state the value returned by each operation from push("weather") onwards.
3 marks
Outline why a stack is appropriate for the Back button.
2 marks
Outline one disadvantage of using a fixed-size array for this stack.
2 marks