Advanced

Object-Oriented Programming

Model domains with classes, instances, and the four OOP pillars.

What you will see

... Rex says woof

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Classes let me model a whole kind of thing. I define a class with an initializer and a method, make an instance and call it, and watch inheritance reuse behaviour in a subclass. I also expose a value with a property, compare shared class state to per-instance state, and see polymorphism let two animals speak their own way.

1

A class bundles data and behaviour. I define __init__ to store name and a method that uses it, then make one instance and print its voice and its type to see the blueprint become a real object.

class Dog: def __init__(self, name): self.name = name def speak(self): return f"{self.name} says woof" dog = Dog("Rex") print(dog.speak()) print(type(dog).__name__)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Rex says woof Dog
Every method takes self as its first parameter; that is the instance the method is called on.
2

A subclass can copy a parent's behaviour and change just one part. I make a Cat inherit Animal, keep the inherited name, override its speak, and print the name and voice to see what was reused and what was replaced.

class Animal: def __init__(self, name): self.name = name class Cat(Animal): def speak(self): return f"{self.name} says meow" c = Cat("Milo") print(c.name) print(c.speak())

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Milo Milo says meow
Cat calls Animal.__init__ automatically, so it does not need its own __init__ just to carry the name.
3

A property turns a method into a value you can read like an attribute. I start with a balance, deposit money, and read it back through the property to see the getter compute the current amount.

class Bank: def __init__(self, balance): self._balance = balance @property def balance(self): return self._balance def deposit(self, amount): self._balance += amount b = Bank(100) b.deposit(50) print(b.balance)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
150
The underscore marks _balance as internal. A real setter would keep out bad values; here only deposit changes it safely.
4

Class attributes are shared by every instance while instance attributes are unique to one. I bump a shared counter, then print the shared value and each object's own value to see what is common and what is personal.

class Counter: count = 0 def __init__(self): self.value = 0 c1 = Counter() c2 = Counter() c1.count += 1 print(c1.count) print(c2.count) print(c1.value)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1 0 0
count is defined on the class, so changing it shows up everywhere. value is per object and stays zero on the other instance.
5

Polymorphism lets every animal speak its own way behind one speak() name. I make a plain Animal and a Dog and print what each says to see the same method call produce different output.

class Animal: def __init__(self, name): self.name = name def speak(self): return "..." class Dog(Animal): def speak(self): return f"{self.name} says woof" cat = Animal("Whiskers") dog = Dog("Rex") print(cat.speak()) print(dog.speak())

Before you run it, say out loud what it will print. Then open the answer.

What it prints
... Rex says woof
The call cat.speak() and dog.speak() look identical, but each resolves to its own method.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A class defines a blueprint; instances hold their own state and call methods that mutate it.
1 Define class with attributes and methods
↓
2 Instantiate -> __init__ sets instance state
↓
3 Call method -> receives self, reads/mutates state
↓
4 Property computes derived value on access
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

class Dog: sets nothing on screen
def __init__(self, name): sets nothing on screen
self.name = name sets nothing on screen
def speak(self): sets nothing on screen
return f"{self.name} says woof" sets nothing on screen
dog = Dog("Rex") sets nothing on screen
print(dog.speak()) Rex says woof
print(type(dog).__name__) Dog
Why this works
The four OOP pillars

Encapsulation hides state and exposes methods. Inheritance reuses and extends behaviour. Polymorphism lets subclasses share an interface with different implementations. Abstraction hides complexity behind simple interfaces.

When you have a choice

Class vs instance state

Class attributes are shared across all instances; instance attributes (set in __init__) are unique per object:

class Counter:
    count = 0          # class attribute, shared
    def __init__(self):
        self.value = 0 # instance attribute

Use class attributes for constants or shared counters, instance attributes for per-object data.

Examples

Defining a class and instance

A simple class

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

    def speak(self):
        return f"{self.name} says woof"

dog = Dog("Rex")
print(dog.speak())   # Rex says woof

__init__ is the constructor; self is the instance. Methods receive the instance as the first argument.

Inheritance

Subclassing

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

class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow"

c = Cat("Milo")
print(c.name, c.speak())   # Milo says meow

Cat inherits Animal's __init__ and overrides speak.

Encapsulation with properties

Controlled attributes

class Bank:
    def __init__(self, balance):
        self._balance = balance   # "private" by convention

    @property
    def balance(self):
        return self._balance

    def deposit(self, amount):
        self._balance += amount

b = Bank(100)
b.deposit(50)
print(b.balance)   # 150

The _ prefix signals private; @property adds computed, safe access.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Functional Programming
Previous Lesson
Functional Programming
Next Lesson
Files and Context Managers
Files and Context Managers →