Classes, Objects, and Encapsulation

You can read this note directly if you know Python variables and functions.

Object-oriented programming adds one central idea: combine the data describing an entity with the methods that operate on that data.

The Beginner Problem OOP Solves

Suppose a program stores student marks using separate variables:

name1 = "Aisha"
mark1 = 72
name2 = "Bo"
mark2 = 45

The program must keep track of which name belongs to which mark. As the number of students and properties grows, the data becomes difficult to organise.

An object keeps the related values together:

s1 = Student("Aisha", 72)
s2 = Student("Bo", 45)

Now:

  • s1 represents one student;
  • s2 represents another student;
  • each object stores its own values;
  • the class can define behaviour shared by all students.

Class Versus Object

A class is a definition or blueprint describing the attributes and methods of a type of object.

An object is a particular instance created from that class.

Caption: Instantiating the Student class creates separate objects that share the same design but store different attribute values.

class Student:
    def __init__(self, name, mark):
        self.name = name
        self.mark = mark
 
    def is_pass(self):
        return self.mark >= 50

Creating objects from the class is called instantiation:

s1 = Student("Aisha", 72)
s2 = Student("Bo", 45)
Objectnamemarkis_pass()
s1"Aisha"72True
s2"Bo"45False

The objects share the same class design, but each object has its own state.

Attributes and Methods

TermMeaningExample
attributedata stored for an object or classself.mark
methoda function defined inside a classis_pass()
instancea particular object created from a classs1
instantiationthe process of creating an objectStudent("Aisha", 72)

A method can read or change the attributes of the current object.

class Counter:
    def __init__(self):
        self.value = 0
 
    def increase(self):
        self.value = self.value + 1

Calling counter.increase() changes the state stored in that particular object.

__init__() and self

At H2 Computing level, __init__() is commonly described as the constructor because it is used to initialise a newly created object.

def __init__(self, name, mark):
    self.name = name
    self.mark = mark

The parameter self refers to the current object.

Read:

self.name = name

as:

store the parameter name in the name attribute of this object

When an instance method is called using dot notation, Python supplies the object as self.

Method callObject used as selfAttribute read
s1.is_pass()s1s1.mark
s2.is_pass()s2s2.mark

This is why self appears in the method definition but is not normally written as an argument in the call.

Local variable versus instance attribute

class Student:
    def __init__(self, name):
        name = name

This does not create a lasting object attribute. The variable name is only local to the method.

The correct form is:

class Student:
    def __init__(self, name):
        self.name = name

The prefix self. attaches the value to the object.

Displaying an Object

Without a suitable string method, printing an object may show a technical representation rather than useful information.

class Student:
    def __init__(self, name, mark):
        self.name = name
        self.mark = mark
 
    def __str__(self):
        return f"{self.name}: {self.mark}"
s1 = Student("Aisha", 72)
print(s1)

Output:

Aisha: 72

__str__() is not one of the named conceptual outcomes in section 2.5, but it is a useful beginner-level Python method for producing readable object output.

Class Diagrams

Caption: Class diagrams show a class name, attributes, and methods.

A simple class diagram contains three sections:

  1. class name;
  2. attributes;
  3. methods.

For example:

Student
----------------
- name
- mark
----------------
+ is_pass()
+ set_mark(mark)

The symbols commonly mean:

SymbolMeaning
-private or internal member
+public member

A class diagram describes the class design. It does not show the different values stored in every object.

Instance Attributes and Class Attributes

An instance attribute belongs to one object.

self.name = name

Different objects may store different values:

s1.name = "Aisha"
s2.name = "Bo"

A class attribute belongs to the class and is shared through the class.

class Student:
    count = 0
 
    def __init__(self, name):
        self.name = name
        Student.count = Student.count + 1

After creating three students, Student.count is 3.

FeatureInstance attributeClass attribute
belongs toone objectthe class
typical accessself.name or s1.nameStudent.count
same value for all objects?not necessarilyshared unless shadowed
suitable useobject-specific stateshared count or setting

A common class-attribute trap

s1.count = 100

This creates or changes an instance attribute named count for s1; it does not reliably update the shared class attribute.

To update the shared value clearly, use:

Student.count = 100

Encapsulation

Encapsulation combines attributes and the methods that operate on them inside one class.

Caption: Encapsulation keeps state and related methods inside the object; outside code should use the public methods rather than changing _balance directly.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance
 
    def deposit(self, amount):
        if amount > 0:
            self._balance = self._balance + amount
            return True
        return False
 
    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance = self._balance - amount
            return True
        return False
 
    def get_balance(self):
        return self._balance

The class stores account data and provides controlled operations on that data.

The withdraw() method can enforce the rule that the balance must not become negative. If outside code changes the balance directly, that rule can be bypassed.

Public Methods and Internal Attributes

A public method is intended to be used by code outside the class.

Examples:

account.deposit(50)
account.withdraw(20)
account.get_balance()

An underscore prefix such as _balance signals that an attribute is intended for internal use:

self._balance = balance

In Python, a single underscore is a convention rather than an absolute access barrier. Outside code can still access the attribute, but doing so ignores the intended interface.

For syllabus explanations, it is useful to distinguish:

  • design intention: internal data should be accessed or changed through public methods;
  • Python behaviour: an underscore does not make access impossible.

Getters and Setters

A getter returns an attribute value.

def get_balance(self):
    return self._balance

A setter changes an attribute value, usually after checking it.

def set_mark(self, mark):
    if 0 <= mark <= 100:
        self._mark = mark
        return True
    return False

Not every attribute needs both a getter and a setter. A setter is useful only when outside code should be allowed to change the value.

A controlled method may also be more meaningful than a general setter. For a bank account, deposit() and withdraw() express the allowed operations more clearly than set_balance().

Information Hiding

Information hiding means that outside code uses the public operations of a class without depending directly on its internal representation.

Preferred:

account.deposit(50)
account.withdraw(20)
balance = account.get_balance()

Avoid:

account._balance = -999

The second statement bypasses the class rules and may leave the object in an invalid state.

Information hiding supports:

  • data integrity;
  • controlled validation;
  • reduced dependence on implementation details;
  • easier modification of the class.

Implementation Independence

Caption: Outside code uses a stable public interface, so the class can replace one internal implementation with another without changing the calling code.

Suppose BankAccount initially stores one number:

self._balance = balance

Later, the class may instead store a list of transactions and calculate the balance from that list.

If the public methods remain:

deposit()
withdraw()
get_balance()

the code using the class may not need to change.

This is implementation independence: users of the class depend on what its public methods do, not on how the class performs the work internally.

Paper 1 and Paper 2 Emphasis

Paper 1-style explanation

Be ready to define and distinguish:

  • class and object;
  • attribute and method;
  • class attribute and instance attribute;
  • encapsulation and information hiding;
  • information hiding and implementation independence.

A strong answer states both the mechanism and the benefit. For example:

Encapsulation combines data and methods in a class. Access through public methods allows the class to validate changes and hides the internal representation from outside code.

Paper 2-style coding

Be ready to:

  • define a class;
  • initialise instance attributes using self;
  • instantiate multiple objects;
  • write methods that read or update state;
  • implement simple getters, setters, or validation methods;
  • use a class attribute for genuinely shared state;
  • trace object state after a sequence of method calls.

Common Mistakes

  • Saying that a class and an object are the same thing.
  • Forgetting self in an instance method definition.
  • Assigning to a local variable instead of an instance attribute.
  • Passing self explicitly in an ordinary method call.
  • Treating a class attribute as though every object has a separate copy.
  • Updating a shared class attribute through an instance and accidentally creating an instance attribute.
  • Claiming that a single underscore makes a Python attribute completely inaccessible.
  • Giving every attribute a setter even when the value should not be changed.
  • Describing information hiding without explaining the public method interface.
  • Mixing methods into the attribute section of a class diagram.

Check Your Understanding

  1. What is the difference between a class and an object?
  2. What does self refer to?
  3. Why does self.name = name create lasting object state but name = name does not?
  4. What is the difference between an instance attribute and a class attribute?
  5. Why should outside code call withdraw() instead of setting _balance directly?
  6. How does information hiding support implementation independence?

Answers:

  1. A class defines a type of object; an object is a particular instance created from the class.
  2. The current object using the method.
  3. self.name stores the value as an attribute of the object, whereas name alone is local to the method.
  4. An instance attribute belongs to one object; a class attribute is associated with the class and used as shared state.
  5. withdraw() can enforce rules and preserve a valid balance.
  6. Outside code depends on public methods, so the internal storage can change without necessarily changing the calling code.