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# 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# 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'], ...)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 .
BankAccount
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)
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.
Inheritance and overriding
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())
Press Run to try your code, then Check answer to grade it.Check yourself
What is self in a method definition?
Tip: use the ← and → keys to move between lessons.