PyPython · Lesson 9 of 14

Classes & OOP

Python classes are simpler than Java or C++ — no access modifiers, no header files, and you can add attributes whenever you want. This is either liberating or terrifying, depending on your background.

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

Python
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
# 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
# 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'], ...)
◆ Note
Use dataclasses for plain data containers. Use regular classes when you need complex initialization, inheritance, or custom descriptors. The @dataclass decorator auto-generates __init__, __repr__, and __eq__ based on your field annotations.