Object-Oriented Programming
High SchoolObject-oriented programming (OOP) organises software around objects — bundles of data (attributes/state) and the behaviour (methods) that acts on it. A class is a blueprint; an object is an instance of that class. OOP rests on four pillars:
- Encapsulation — bundle data with the methods that manage it, and hide internal detail behind a public interface. The outside world uses the object without depending on how it works inside.
- Abstraction — expose what an object does, conceal how. A
BankAccountexposeswithdraw(), not the ledger internals. - Inheritance — a subclass reuses and extends a parent class, modelling an "is-a" relationship (a
Dogis anAnimal). - Polymorphism — one interface, many implementations: calling
speak()on different animals runs different code, chosen at runtime by the object's actual type.
OOP is fundamentally a strategy for managing complexity in large programs. By grouping state with the operations that maintain it, an object can enforce its own invariants — e.g. a bank balance that never goes negative — so no external code can corrupt it. This is encapsulation's real payoff: local reasoning. You can trust an object because it guards itself.
Polymorphism is the pillar that pays dividends at scale. Write code that calls shape.area() and it works for circles, squares, and triangles you have not even written yet — as long as each provides an area(). New types slot in without touching old code. This "open for extension, closed for modification" principle is why OOP dominated large-system design for decades.
Caveat. OOP is a tool, not a religion. Deep inheritance hierarchies often cause more pain than they cure; modern practice favours composition over inheritance — build objects from smaller parts rather than inheriting from towering ancestors.
We model animals to show all four pillars: a base class, a private-by-convention attribute (encapsulation), two subclasses (inheritance), and a shared method behaving differently (polymorphism).
class Animal:
def __init__(self, name):
self._name = name # leading underscore: "internal, please don't touch"
def name(self): # public accessor -> encapsulation
return self._name
def speak(self):
raise NotImplementedError # abstract: each subclass must define this
class Dog(Animal): # Dog IS-A Animal -> inheritance
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
class Cow(Animal):
def speak(self):
return "Moo"
# Polymorphism: identical call, type-specific behaviour chosen at runtime
animals = [Dog("Rex"), Cat("Milo"), Cow("Daisy")]
for a in animals:
print(a.name() + " says " + a.speak())
# Output:
# Rex says Woof
# Milo says Meow
# Daisy says Moo
The loop never asks "what kind of animal is this?" — it simply calls speak() and each object supplies its own answer. Add a Duck class tomorrow and the loop works unchanged. That is polymorphism buying you extensibility.
Q1 What is the difference between a class and an object?
A class is a template — a definition of what attributes and methods instances will have. An object is a concrete instance created from that template, with its own actual data. Dog is the class; Rex and Fido are two distinct objects, each with its own name, both sharing the speak() behaviour defined once in the class.
Q2 Why hide data behind methods instead of accessing it directly?
Encapsulation lets the object enforce rules. If balance is only changed through deposit() and withdraw(), those methods can reject invalid operations (a negative withdrawal, an overdraft) and maintain invariants. Expose the raw field and any code anywhere can violate them, making bugs impossible to localise.
Q3 What does "composition over inheritance" mean?
Rather than making a Car inherit from Engine, give the car an engine as a component (self.engine = Engine()). Composition ("has-a") is more flexible than inheritance ("is-a"): you can swap parts, mix behaviours, and avoid the brittle, deep hierarchies that make inheritance hard to change. Inheritance is best reserved for genuine "is-a" relationships.
Q4 How does polymorphism actually pick the right method?
Through dynamic dispatch: at runtime, the object carries a reference to its class, and the language looks up speak() on that class (and its ancestors). So the method chosen depends on the object's actual type, not the type of the variable holding it. This late binding is what lets one line of code serve many types.
How the ideas connect
Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.
The key facts, visualised
Worked problems, step by step
Follow each solution line by line, then try to reproduce it on paper before moving on.
Example 1A class Dog has method speak() returning "Woof". Cat.speak() returns "Meow". You call speak() on an Animal that is a Cat. What is returned?
- The variable is typed Animal but holds a Cat.
- Polymorphism picks the Cat implementation at runtime.
Example 2Circle inherits Shape and overrides area(). For radius 2, area = 3.14 * r^2. Compute.
- area = 3.14 * 2^2.
- 3.14 * 4.
Now you try
Work each one out first, then tap to reveal the worked answer.