27. Simple OOP with classes

Chapter 26 put a few methods on one object created by hand. That works for one object, but wastes effort for many objects of the same shape. Imagine 100 dogs — copying bark onto each one separately is impractical. This chapter shows the Python class as a blueprint: you define it once, then call it like a function to create as many instances as you want.

A class with __init__ and methods

Here is a Point class. Read it, then check the explanation:

import math

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def distance(self, other):
        dx = self.x - other.x
        dy = self.y - other.y
        return math.sqrt(dx * dx + dy * dy)

a = Point(0, 0)
b = Point(3, 4)
print(a.distance(b))   # 5.0

What is going on:

  1. class Point: declares the class — a blueprint. All methods are indented inside it.
  2. __init__(self, x, y) is the constructor. Python calls it automatically whenever you write Point(0, 0). It sets up the instance's fields.
  3. self is the instance being created or used. Writing self.x = x stores x on the instance itself.
  4. Point(0, 0) creates a new instance — a separate object with its own x and y.
  5. def distance(self, other) is a regular method. self receives the instance the method was called on; other is the argument you passed in.
  6. a.distance(b) calls the method. Python fills in self = a automatically; you only pass other = b.

The result: one class definition, many small instances, no copying.

Open exercises/27/01-point.py. Add a move(self, dx, dy) method that adds dx to self.x and dy to self.y. Create a point, move it twice, then print its position.

Why instances and not just dictionaries?

A plain dictionary can hold the same data, but a class groups the data and the behaviour together in one place. The class also acts as a factory: each call to Point(x, y) produces a fresh, independent object. Two instances never share fields unless you explicitly make them do so.

A second class: Character

A small character class that can take damage:

class Character:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp
        self.max_hp = hp

    def take_damage(self, amount):
        self.hp = self.hp - amount
        if self.hp < 0:
            self.hp = 0

    def heal(self, amount):
        self.hp = self.hp + amount
        if self.hp > self.max_hp:
            self.hp = self.max_hp

    def is_alive(self):
        return self.hp > 0

c = Character("Keiko", 100)
c.take_damage(30)
print(c.hp)            # 70
c.heal(50)
print(c.hp)            # 100  (capped at max_hp)
print(c.is_alive())    # True

Each Character(name, hp) call produces a fresh instance with its own name, hp, and max_hp. All the methods live inside the class and are shared by every instance.

Inheritance: one class building on another

A child class inherits all the methods of a parent by naming it in parentheses:

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

    def describe(self):
        print(f"I am {self.name}.")

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name)   # run Animal's __init__

    def bark(self):
        print(f"{self.name}: Woof!")

rex = Dog("Rex")
rex.describe()    # I am Rex.   (inherited from Animal)
rex.bark()        # Rex: Woof!  (defined on Dog)

class Dog(Animal): says Dog inherits from Animal. super().__init__(name) calls Animal's constructor so the name field is set up correctly.

rex.describe() works even though Dog does not define it — Python finds it on Animal automatically.

That is enough inheritance for most purposes. The next chapters go deeper into overriding and designing a class well.

Homework

Problem 1 — Point class with move

Open exercises/27/homework/01-point.py. Build a Point class as above, plus a move(dx, dy) method that adds the deltas to the position. Test by creating two points, moving one, then printing the distance between them.

Problem 2 — Character class

Open exercises/27/homework/02-character.py. Build the Character class with __init__(name, hp), take_damage(amount), heal(amount), and is_alive(). Add a report() method that prints the name and current HP. Walk through a small fight: damage twice, heal once, and print the report after each action.

Problem 3 — Rectangle class

Open exercises/27/homework/03-rectangle.py. Build a Rectangle class with __init__(width, height), area(), and perimeter(). Test it with two rectangles of different sizes.

Challenge — Animal and Dog

Open exercises/27/homework/04-inheritance.py. Build Animal and Dog exactly as in the chapter's inheritance example. Then add a second child class Cat that also inherits from Animal and has its own meow() method. Create one dog and one cat, call describe() on both, then bark() on the dog and meow() on the cat.

Stuck or finished? Open the homework solutions page.