Lesson 9 of 34 · 0 of 2 exercises solved

Classes & OOP

Classes let you bundle data and the that work on it into your own types. Python classes are lighter than those in Java or C++: no access modifiers and no header files.

A class is a blueprint for creating objects. Python uses the __init__ as the constructor. The is the — Python passes it automatically, but you must declare it explicitly in every method.

class Animal:
    # Class variable — shared by all instances
    kingdom = "Animalia"

    def __init__(self, name: str, sound: str):
        # Instance variables — unique to each instance
        self.name = name
        self.sound = sound
        self._age = 0  # _ prefix = "private by convention"

    def speak(self) -> str:
        return f"{self.name} says {self.sound}!"

    def birthday(self):
        self._age += 1

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value: int):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

    def __repr__(self):
        return f"Animal(name={self.name!r}, sound={self.sound!r})"

    def __str__(self):
        return self.name


dog = Animal("Rex", "woof")
print(dog.speak())       # Rex says woof!
print(dog.age)           # 0
dog.birthday()
print(dog.age)           # 1
print(repr(dog))         # Animal(name='Rex', sound='woof')
print(str(dog))          # Rex
Python (loads on first run)Ctrl+Enter runs
# Inheritance
class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name, "woof")  # call parent __init__
        self.breed = breed

    def speak(self) -> str:
        # Override parent method
        return f"{self.name} the {self.breed} barks!"

    def fetch(self, item: str) -> str:
        return f"{self.name} fetched the {item}!"


class Cat(Animal):
    def __init__(self, name: str):
        super().__init__(name, "meow")
        self.lives = 9

    def speak(self) -> str:
        return f"{self.name} ignores you."


# Polymorphism — same interface, different behavior
animals: list[Animal] = [
    Dog("Buddy", "Labrador"),
    Cat("Whiskers"),
    Dog("Max", "Poodle"),
]

for animal in animals:
    print(animal.speak())

# isinstance checks
print(isinstance(animals[0], Dog))     # True
print(isinstance(animals[0], Animal))  # True
print(isinstance(animals[1], Dog))     # False
Python (loads on first run)Ctrl+Enter runs
# Dataclasses — less boilerplate for simple classes
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Point:
    x: float
    y: float

    def distance_to(self, other: 'Point') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5


@dataclass
class Player:
    name: str
    hp: int = 100
    inventory: list[str] = field(default_factory=list)  # mutable default!
    position: Point = field(default_factory=lambda: Point(0, 0))

    def pick_up(self, item: str):
        self.inventory.append(item)

    def __post_init__(self):
        if self.hp < 0:
            raise ValueError("HP cannot be negative")


p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0

player = Player("Alice")
player.pick_up("sword")
player.pick_up("shield")
print(player)  # Player(name='Alice', hp=100, inventory=['sword', 'shield'], ...)
Python (loads on first run)Ctrl+Enter runs

Common question

Should I use classes for every kind of data?

A class bundles state (attributes) with the operations that keep that state consistent (methods). Its constructor __init__ things up correctly; its methods keep them that way; its dunder methods (__repr__, __eq__, __len__) let instances behave like built-in values. A good sign you need one: you keep passing the same to the same handful of functions. That dictionary wants to be an object.

Prefer small classes with clear jobs, dataclasses for plain records, properties for computed or validated attributes, and composition over inheritance. Inherit only when a subclass really is a special case of the parent and can be used anywhere the parent is expected.

Source: Alan Kay, who coined the term: "OOP to me means only messaging, local retention and protection and hiding of state-." Classes are a means, not the point.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • Everything in Python is an object, so every piece of data deserves its own class.

  • self is a special keyword that Python treats magically.

  • Inheritance is the main way to reuse code, so deep class hierarchies mean good design.

Practice

Build a BankAccount class. The constructor takes an owner name and an optional starting balance. deposit(amount) and withdraw(amount) adjust the balance; withdrawing more than you have should raise ValueError. Expose the balance through a read-only .

Exercise

BankAccount

4 checks
class BankAccount:
    def __init__(self, owner, balance=0):
        ...

    @property
    def balance(self):
        ...

    def deposit(self, amount):
        ...

    def withdraw(self, amount):
        ...


acct = BankAccount("Ada", 100)
acct.deposit(50)
acct.withdraw(30)
print(acct.owner, acct.balance)
 
Python (loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Create an Animal base class with a name and a speak() method that returns "...". Then write Dog and Cat subclasses that override speak() to return "Woof" and "Meow". Each animal should describe itself as e.g. Rex says Woof via a describe() method defined once on the base class.

Exercise

Inheritance and overriding

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

    def speak(self):
        return "..."

    def describe(self):
        ...


class Dog(Animal):
    ...


class Cat(Animal):
    ...


for animal in [Dog("Rex"), Cat("Tom"), Animal("Blob")]:
    print(animal.describe())
 
Python (loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

What is self in a method definition?

Finished reading?2 exercises still open. You can come back to them later.

Tip: use the ← and → keys to move between lessons.