Redirect Chapter 5: Object-Oriented Python | AI Fundamentals
← Back to Tutorials Chapter 5

Object-Oriented Python

So far we have written functions that operate on data. Object-oriented programming (OOP) flips that: it bundles data and the functions that act on it into a single unit called an object. This chapter introduces the four pillars of OOP through small, runnable examples.

Classes and Objects

A class is a blueprint; an object is a concrete instance built from that blueprint. The class says what data a robot has and what it can do; each robot object carries its own data.

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

    def greet(self):
        return "Hello, I am " + self.name

r1 = Robot("R2")
r2 = Robot("C3")
print(r1.greet())   # Hello, I am R2
print(r2.greet())   # Hello, I am C3

The __init__ method runs automatically when you create an object. Its first parameter, self, refers to the particular object being built, so each instance keeps its own name.

Attributes and Methods

Class vs instance attributes

An instance attribute belongs to one object. A class attribute is shared by every object of the class:

class Dog:
    species = "Canis familiaris"   # class attribute

    def __init__(self, name):
        self.name = name           # instance attribute

d1 = Dog("Bruno")
d2 = Dog("Rex")
print(d1.species)   # Canis familiaris - shared
print(d2.name)      # Rex - own value

Methods

A method is a function defined inside a class. It always receives the object as its first argument, conventionally named self:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

c = Counter()
print(c.increment())   # 1
print(c.increment())   # 2
Common mistake: forgetting self in the method signature or omitting it when reading attributes. Every attribute access inside a method needs self. in front of it.

Classes Practice

  1. Create a BankAccount class with balance, deposit(amount), and withdraw(amount) methods that refuse to go below zero.
  2. Create a Student class storing name and a list of grades, with a method returning the average grade.
  3. Create a Book class with title, author, and a flag available, plus methods to borrow and return it.
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance

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

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

acc = BankAccount(100)
acc.deposit(50)
acc.withdraw(200)   # Insufficient funds
print(acc.balance)  # 150

Inheritance

Inheritance lets a new class reuse the code of an existing one. The child class gets every attribute and method of the parent, and can add or replace its own.

class Animal:
    def speak(self):
        return "Some sound"

class Cat(Animal):
    def speak(self):
        return "Meow"

class Dog(Animal):
    def speak(self):
        return "Woof"

animals = [Cat(), Dog(), Animal()]
for a in animals:
    print(a.speak())   # Meow / Woof / Some sound

Here Cat and Dog inherit from Animal but override speak. Shared logic stays in the parent; differences live in the children.

Polymorphism

Polymorphism means "many forms": the same method name behaves differently depending on the object type. The loop above is a working example — speak() on each object produces a different result even though the call looks identical.

def announce(creature):
    print(creature.speak())

announce(Cat())   # Meow
announce(Dog())   # Woof

Because every animal promises a speak() method, one function works for all of them.

Encapsulation

Encapsulation hides internal details behind a clean interface, so callers use methods instead of touching raw data. Python signals a private member with a leading underscore — it is a convention, not a hard wall:

class Thermostat:
    def __init__(self):
        self._temp_c = 22

    def raise_temp(self, delta):
        self._temp_c += delta

    def current(self):
        return self._temp_c

t = Thermostat()
t.raise_temp(3)
print(t.current())   # 25
Why encapsulate? If the temperature must stay within a range, the validation lives in one place — the methods — instead of being sprinkled across the code that uses the class.

Abstraction

Abstraction shows only what matters and hides how things are done. The user of a Thermostat calls raise_temp without knowing whether the wiring is electric or gas. In code, the abc module enforces this with abstract base classes that cannot be instantiated and must be implemented by subclasses:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.area())   # 16

Any class that forgets to implement area() fails immediately, which keeps the contract honest.

OOP Practice Assignments

  1. Build a Vehicle parent class with fuel and move(), then Car and Bicycle children that override move() differently.
  2. Add a class attribute vehicle_count to Vehicle that increments every time a new object is created, proving the attribute is shared.
  3. Design a BankAccount hierarchy: a parent Account with balance and interest(), and a SavingsAccount child that overrides the interest calculation.
Exercise: Model a library. Create an Item base class (title, availability), then Book and DVD subclasses with different loan_period values. Add encapsulation by keeping availability private with public borrow()/return_item() methods, and verify both subclasses work through one shared function that only calls the public interface.