IntermediatePython ยท Lesson 1

Object-Oriented Programming

Master classes, objects, inheritance, encapsulation, and polymorphism in Python

Classes and Objects

class Dog:
    # Class variable (shared by all instances)
    species = "Canis familiaris"

    # Constructor
    def __init__(self, name, age):
        # Instance variables
        self.name = name
        self.age = age

    # Instance method
    def bark(self):
        return f"{self.name} says: Woof!"

    def __str__(self):
        return f"Dog({self.name}, age={self.age})"

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

# Creating instances
rex = Dog("Rex", 3)
buddy = Dog("Buddy", 5)

print(rex.bark())       # Rex says: Woof!
print(rex.species)      # Canis familiaris
print(str(rex))         # Dog(Rex, age=3)
print(Dog.species)      # Canis familiaris โ€” access via class

Inheritance

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

    def speak(self):
        raise NotImplementedError("Subclass must implement speak()")

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


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


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


class Duck(Animal):
    def speak(self):
        return "Quack!"


# Polymorphism
animals = [Dog("Rex"), Cat("Whiskers"), Duck("Donald")]
for animal in animals:
    print(f"{animal.name}: {animal.speak()}")

super()

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def info(self):
        return f"{self.year} {self.make} {self.model}"


class ElectricCar(Vehicle):
    def __init__(self, make, model, year, battery_kwh):
        super().__init__(make, model, year)   # call parent __init__
        self.battery_kwh = battery_kwh

    def info(self):
        base = super().info()
        return f"{base} (Electric, {self.battery_kwh}kWh)"


tesla = ElectricCar("Tesla", "Model 3", 2023, 82)
print(tesla.info())  # 2023 Tesla Model 3 (Electric, 82kWh)

Multiple Inheritance and MRO

class A:
    def hello(self):
        return "Hello from A"

class B(A):
    def hello(self):
        return "Hello from B"

class C(A):
    def hello(self):
        return "Hello from C"

class D(B, C):
    pass

d = D()
print(d.hello())   # "Hello from B" โ€” follows MRO
print(D.__mro__)   # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, ...)

Encapsulation

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance     # "protected" โ€” convention
        self.__pin = "1234"         # "private" โ€” name mangling

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, amount):
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount
        return self._balance

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        return self._balance


account = BankAccount("Alice", 1000)
print(account.balance)        # 1000 โ€” uses @property getter
account.balance = 2000        # uses @property setter
account.deposit(500)
print(account.balance)        # 2500

Class and Static Methods

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32

    @classmethod
    def from_fahrenheit(cls, f):
        """Alternative constructor"""
        return cls((f - 32) * 5/9)

    @staticmethod
    def is_freezing(celsius):
        """Doesn't need self or cls"""
        return celsius <= 0


boiling = Temperature(100)
print(boiling.fahrenheit)          # 212.0
body_temp = Temperature.from_fahrenheit(98.6)
print(round(body_temp.celsius, 1)) # 37.0
print(Temperature.is_freezing(-5)) # True

Abstract Classes

from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        pass

    def describe(self):
        return f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}"


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

    def perimeter(self):
        import math
        return 2 * math.pi * self.radius


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)


shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.describe())
# Area=78.54, Perimeter=31.42
# Area=24.00, Perimeter=20.00

Dunder Methods

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

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __abs__(self):
        return (self.x**2 + self.y**2)**0.5

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2)    # Vector(3, 7)
print(v1 * 2)     # Vector(4, 6)
print(abs(v1))    # 3.605...

Exercises

Exercise 1: Stack Class

Implement a Stack class with push, pop, peek, is_empty, and size methods.

Solution:

class Stack:
    def __init__(self):
        self._data = []

    def push(self, item):
        self._data.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data[-1]

    def is_empty(self):
        return len(self._data) == 0

    def size(self):
        return len(self._data)

    def __repr__(self):
        return f"Stack({self._data})"


s = Stack()
s.push(1)
s.push(2)
s.push(3)
print(s.pop())   # 3
print(s.peek())  # 2
print(s.size())  # 2

Exercise 2: Linked List

Build a simple singly linked list with append, prepend, delete, and display.

Solution:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        node = Node(data)
        if not self.head:
            self.head = node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = node

    def prepend(self, data):
        node = Node(data)
        node.next = self.head
        self.head = node

    def delete(self, data):
        if not self.head:
            return
        if self.head.data == data:
            self.head = self.head.next
            return
        current = self.head
        while current.next:
            if current.next.data == data:
                current.next = current.next.next
                return
            current = current.next

    def __repr__(self):
        items = []
        current = self.head
        while current:
            items.append(str(current.data))
            current = current.next
        return " -> ".join(items)


ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
print(ll)       # 0 -> 1 -> 2 -> 3
ll.delete(2)
print(ll)       # 0 -> 1 -> 3