Skip to main content

Chapter 5: Object-Oriented Programming - Master Classes, Objects, and Inheritance

Welcome to the comprehensive guide to Python Object-Oriented Programming (OOP)! This chapter covers classes, objects, inheritance, polymorphism, and encapsulation - the fundamental concepts that make Python a powerful object-oriented language. Understanding OOP is crucial for building complex, maintainable applications.

Learning Objectives​

By the end of this chapter, you will understand:

  • Classes and objects - the foundation of OOP
  • Attributes and methods - data and behavior in classes
  • Constructors and destructors - object lifecycle management
  • Inheritance - creating class hierarchies
  • Polymorphism - different behaviors for different objects
  • Encapsulation - data hiding and access control
  • Special methods - magic methods for custom behavior
  • Class methods and static methods - alternative method types
  • Property decorators - controlled attribute access
  • Best practices for OOP design in Python

Classes and Objects​

Basic Class Definition​

A class is a blueprint for creating objects:

# Basic class definition
class Person:
"""A simple Person class."""

def __init__(self, name, age):
"""Initialize a Person object."""
self.name = name
self.age = age

def introduce(self):
"""Introduce the person."""
return f"Hi, I'm {self.name} and I'm {self.age} years old."

def have_birthday(self):
"""Increase age by 1."""
self.age += 1
return f"Happy birthday! {self.name} is now {self.age} years old."

# Creating objects (instances)
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

# Using objects
print(person1.introduce()) # Hi, I'm Alice and I'm 25 years old.
print(person2.introduce()) # Hi, I'm Bob and I'm 30 years old.

print(person1.have_birthday()) # Happy birthday! Alice is now 26 years old.

Class Attributes vs Instance Attributes​

class Student:
"""Student class with class and instance attributes."""

# Class attribute (shared by all instances)
school_name = "Python University"
total_students = 0

def __init__(self, name, student_id, major):
"""Initialize a Student object."""
# Instance attributes (unique to each instance)
self.name = name
self.student_id = student_id
self.major = major
self.grades = []

# Increment class attribute
Student.total_students += 1

def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)

def get_average(self):
"""Calculate average grade."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)

@classmethod
def get_total_students(cls):
"""Get total number of students."""
return cls.total_students

# Creating students
student1 = Student("Alice", "S001", "Computer Science")
student2 = Student("Bob", "S002", "Mathematics")

# Accessing class attributes
print(f"School: {Student.school_name}")
print(f"Total students: {Student.get_total_students()}")

# Accessing instance attributes
student1.add_grade(85)
student1.add_grade(92)
student1.add_grade(78)

print(f"{student1.name}'s average: {student1.get_average():.2f}")

Methods in Classes​

Instance Methods​

class BankAccount:
"""Bank account class with instance methods."""

def __init__(self, account_holder, initial_balance=0):
"""Initialize a bank account."""
self.account_holder = account_holder
self.balance = initial_balance
self.transaction_history = []

def deposit(self, amount):
"""Deposit money into the account."""
if amount > 0:
self.balance += amount
self.transaction_history.append(f"Deposit: +${amount}")
return f"Deposited ${amount}. New balance: ${self.balance}"
else:
return "Deposit amount must be positive"

def withdraw(self, amount):
"""Withdraw money from the account."""
if amount > 0:
if amount <= self.balance:
self.balance -= amount
self.transaction_history.append(f"Withdrawal: -${amount}")
return f"Withdrew ${amount}. New balance: ${self.balance}"
else:
return "Insufficient funds"
else:
return "Withdrawal amount must be positive"

def get_balance(self):
"""Get current balance."""
return self.balance

def get_transaction_history(self):
"""Get transaction history."""
return self.transaction_history.copy()

# Using the class
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
print(f"Current balance: ${account.get_balance()}")
print("Transaction history:", account.get_transaction_history())

Class Methods​

class Date:
"""Date class with class methods."""

def __init__(self, year, month, day):
"""Initialize a Date object."""
self.year = year
self.month = month
self.day = day

@classmethod
def from_string(cls, date_string):
"""Create Date object from string (YYYY-MM-DD)."""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)

@classmethod
def today(cls):
"""Create Date object for today."""
import datetime
today = datetime.date.today()
return cls(today.year, today.month, today.day)

def __str__(self):
"""String representation of the date."""
return f"{self.year}-{self.month:02d}-{self.day:02d}"

# Using class methods
date1 = Date.from_string("2024-01-15")
date2 = Date.today()

print(f"Date from string: {date1}")
print(f"Today's date: {date2}")

Static Methods​

class MathUtils:
"""Math utilities class with static methods."""

@staticmethod
def add(a, b):
"""Add two numbers."""
return a + b

@staticmethod
def multiply(a, b):
"""Multiply two numbers."""
return a * b

@staticmethod
def is_prime(n):
"""Check if a number is prime."""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True

@staticmethod
def factorial(n):
"""Calculate factorial of a number."""
if n < 0:
raise ValueError("Factorial is not defined for negative numbers")
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)

# Using static methods
print(f"5 + 3 = {MathUtils.add(5, 3)}")
print(f"4 * 7 = {MathUtils.multiply(4, 7)}")
print(f"Is 17 prime? {MathUtils.is_prime(17)}")
print(f"5! = {MathUtils.factorial(5)}")

Inheritance​

Basic Inheritance​

# Parent class
class Animal:
"""Base Animal class."""

def __init__(self, name, species):
"""Initialize an Animal."""
self.name = name
self.species = species
self.energy = 100

def eat(self, food):
"""Animal eats food."""
self.energy += 10
return f"{self.name} ate {food} and gained energy"

def sleep(self):
"""Animal sleeps."""
self.energy += 20
return f"{self.name} slept and restored energy"

def make_sound(self):
"""Make a generic animal sound."""
return f"{self.name} makes a sound"

# Child class
class Dog(Animal):
"""Dog class inheriting from Animal."""

def __init__(self, name, breed):
"""Initialize a Dog."""
super().__init__(name, "Canine") # Call parent constructor
self.breed = breed

def make_sound(self):
"""Override parent method."""
return f"{self.name} barks: Woof! Woof!"

def fetch(self, item):
"""Dog-specific method."""
self.energy -= 5
return f"{self.name} fetched the {item}"

# Another child class
class Cat(Animal):
"""Cat class inheriting from Animal."""

def __init__(self, name, color):
"""Initialize a Cat."""
super().__init__(name, "Feline")
self.color = color

def make_sound(self):
"""Override parent method."""
return f"{self.name} meows: Meow! Meow!"

def climb(self, height):
"""Cat-specific method."""
self.energy -= 3
return f"{self.name} climbed {height} feet"

# Using inheritance
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.make_sound()) # Buddy barks: Woof! Woof!
print(cat.make_sound()) # Whiskers meows: Meow! Meow!

print(dog.fetch("ball"))
print(cat.climb(5))

print(f"Dog energy: {dog.energy}")
print(f"Cat energy: {cat.energy}")

Multiple Inheritance​

# Multiple parent classes
class Flyable:
"""Mixin class for flying ability."""

def fly(self):
"""Fly method."""
return f"{self.name} is flying"

class Swimmable:
"""Mixin class for swimming ability."""

def swim(self):
"""Swim method."""
return f"{self.name} is swimming"

class Duck(Animal, Flyable, Swimmable):
"""Duck class with multiple inheritance."""

def __init__(self, name):
"""Initialize a Duck."""
Animal.__init__(self, name, "Bird")

def make_sound(self):
"""Override parent method."""
return f"{self.name} quacks: Quack! Quack!"

# Using multiple inheritance
duck = Duck("Donald")
print(duck.make_sound()) # Donald quacks: Quack! Quack!
print(duck.fly()) # Donald is flying
print(duck.swim()) # Donald is swimming

Encapsulation​

Private and Protected Attributes​

class BankAccount:
"""Bank account with encapsulation."""

def __init__(self, account_holder, initial_balance=0):
"""Initialize a bank account."""
self.account_holder = account_holder
self._balance = initial_balance # Protected attribute
self.__account_number = self._generate_account_number() # Private attribute

def _generate_account_number(self):
"""Generate a private account number."""
import random
return f"ACC{random.randint(100000, 999999)}"

def get_balance(self):
"""Public method to get balance."""
return self._balance

def deposit(self, amount):
"""Public method to deposit money."""
if amount > 0:
self._balance += amount
return True
return False

def withdraw(self, amount):
"""Public method to withdraw money."""
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False

def get_account_info(self):
"""Get account information (excluding private data)."""
return {
"holder": self.account_holder,
"balance": self._balance
}

# Using encapsulation
account = BankAccount("Alice", 1000)

# Accessing public methods
print(f"Balance: ${account.get_balance()}")
account.deposit(500)
print(f"Balance after deposit: ${account.get_balance()}")

# Protected attribute (accessible but not recommended)
print(f"Protected balance: {account._balance}")

# Private attribute (not directly accessible)
# print(account.__account_number) # This would cause an error

Property Decorators​

class Temperature:
"""Temperature class with property decorators."""

def __init__(self, celsius=0):
"""Initialize temperature in Celsius."""
self._celsius = celsius

@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius

@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value

@property
def fahrenheit(self):
"""Get temperature in Fahrenheit."""
return self._celsius * 9/5 + 32

@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit."""
self.celsius = (value - 32) * 5/9

@property
def kelvin(self):
"""Get temperature in Kelvin."""
return self._celsius + 273.15

@kelvin.setter
def kelvin(self, value):
"""Set temperature in Kelvin."""
if value < 0:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value - 273.15

# Using properties
temp = Temperature(25)
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")
print(f"Kelvin: {temp.kelvin}K")

# Setting temperature using different units
temp.fahrenheit = 86
print(f"After setting 86°F: {temp.celsius}°C")

temp.kelvin = 300
print(f"After setting 300K: {temp.celsius}°C")

Special Methods (Magic Methods)​

Common Special Methods​

class Vector:
"""Vector class with special methods."""

def __init__(self, x, y):
"""Initialize a vector."""
self.x = x
self.y = y

def __str__(self):
"""String representation for users."""
return f"Vector({self.x}, {self.y})"

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

def __add__(self, other):
"""Add two vectors."""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented

def __sub__(self, other):
"""Subtract two vectors."""
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented

def __mul__(self, scalar):
"""Multiply vector by scalar."""
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented

def __eq__(self, other):
"""Check if two vectors are equal."""
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False

def __len__(self):
"""Return the magnitude of the vector."""
return int((self.x**2 + self.y**2)**0.5)

def __bool__(self):
"""Return True if vector is not zero."""
return self.x != 0 or self.y != 0

# Using special methods
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1: {v1}") # Vector(3, 4)
print(f"v2: {v2}") # Vector(1, 2)

v3 = v1 + v2
print(f"v1 + v2: {v3}") # Vector(4, 6)

v4 = v1 * 2
print(f"v1 * 2: {v4}") # Vector(6, 8)

print(f"v1 == v2: {v1 == v2}") # False
print(f"Length of v1: {len(v1)}") # 5
print(f"v1 is truthy: {bool(v1)}") # True

Context Manager Methods​

class FileManager:
"""File manager with context manager methods."""

def __init__(self, filename, mode):
"""Initialize file manager."""
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
"""Enter the context manager."""
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit the context manager."""
if self.file:
self.file.close()
if exc_type:
print(f"Exception occurred: {exc_val}")
return False # Don't suppress exceptions

# Using context manager
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")

# File is automatically closed

Polymorphism​

Method Overriding​

class Shape:
"""Base Shape class."""

def area(self):
"""Calculate area (to be overridden)."""
raise NotImplementedError("Subclass must implement area method")

def perimeter(self):
"""Calculate perimeter (to be overridden)."""
raise NotImplementedError("Subclass must implement perimeter method")

class Rectangle(Shape):
"""Rectangle class."""

def __init__(self, width, height):
"""Initialize rectangle."""
self.width = width
self.height = height

def area(self):
"""Calculate rectangle area."""
return self.width * self.height

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

class Circle(Shape):
"""Circle class."""

def __init__(self, radius):
"""Initialize circle."""
self.radius = radius

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

def perimeter(self):
"""Calculate circle perimeter (circumference)."""
import math
return 2 * math.pi * self.radius

# Polymorphism in action
shapes = [
Rectangle(5, 3),
Circle(4),
Rectangle(2, 8)
]

for shape in shapes:
print(f"{type(shape).__name__}: Area = {shape.area():.2f}, Perimeter = {shape.perimeter():.2f}")

Duck Typing​

class Duck:
"""Duck class."""

def quack(self):
return "Quack!"

def fly(self):
return "Flying with wings"

class Airplane:
"""Airplane class."""

def quack(self):
return "I'm not a duck, but I can make noise"

def fly(self):
return "Flying with engines"

def make_it_quack_and_fly(thing):
"""Function that works with any object that has quack and fly methods."""
print(f"Quack: {thing.quack()}")
print(f"Fly: {thing.fly()}")

# Duck typing - objects are used based on their methods, not their type
duck = Duck()
airplane = Airplane()

make_it_quack_and_fly(duck)
make_it_quack_and_fly(airplane)

Advanced OOP Concepts​

Abstract Base Classes​

from abc import ABC, abstractmethod

class Animal(ABC):
"""Abstract base class for animals."""

def __init__(self, name):
"""Initialize animal."""
self.name = name

@abstractmethod
def make_sound(self):
"""Abstract method - must be implemented by subclasses."""
pass

def sleep(self):
"""Concrete method - can be used by all subclasses."""
return f"{self.name} is sleeping"

class Dog(Animal):
"""Dog class implementing Animal."""

def make_sound(self):
"""Implement abstract method."""
return f"{self.name} barks"

class Cat(Animal):
"""Cat class implementing Animal."""

def make_sound(self):
"""Implement abstract method."""
return f"{self.name} meows"

# Using abstract base classes
dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.make_sound())
print(cat.make_sound())
print(dog.sleep())

Composition vs Inheritance​

# Composition example
class Engine:
"""Engine class."""

def __init__(self, horsepower):
"""Initialize engine."""
self.horsepower = horsepower

def start(self):
"""Start the engine."""
return "Engine started"

def stop(self):
"""Stop the engine."""
return "Engine stopped"

class Car:
"""Car class using composition."""

def __init__(self, make, model, engine_horsepower):
"""Initialize car."""
self.make = make
self.model = model
self.engine = Engine(engine_horsepower) # Composition

def start(self):
"""Start the car."""
return f"{self.make} {self.model}: {self.engine.start()}"

def stop(self):
"""Stop the car."""
return f"{self.make} {self.model}: {self.engine.stop()}"

# Using composition
car = Car("Toyota", "Camry", 200)
print(car.start())
print(car.stop())

Practical Examples​

Example 1: Library Management System​

class Book:
"""Book class."""

def __init__(self, title, author, isbn, copies=1):
"""Initialize a book."""
self.title = title
self.author = author
self.isbn = isbn
self.copies = copies
self.available_copies = copies

def borrow(self):
"""Borrow a copy of the book."""
if self.available_copies > 0:
self.available_copies -= 1
return True
return False

def return_book(self):
"""Return a copy of the book."""
if self.available_copies < self.copies:
self.available_copies += 1
return True
return False

def is_available(self):
"""Check if book is available."""
return self.available_copies > 0

def __str__(self):
"""String representation."""
return f"{self.title} by {self.author} (ISBN: {self.isbn})"

class Member:
"""Library member class."""

def __init__(self, name, member_id):
"""Initialize a member."""
self.name = name
self.member_id = member_id
self.borrowed_books = []

def borrow_book(self, book):
"""Borrow a book."""
if book.borrow():
self.borrowed_books.append(book)
return f"Successfully borrowed {book.title}"
return f"Sorry, {book.title} is not available"

def return_book(self, book):
"""Return a book."""
if book in self.borrowed_books:
book.return_book()
self.borrowed_books.remove(book)
return f"Successfully returned {book.title}"
return f"You haven't borrowed {book.title}"

def get_borrowed_books(self):
"""Get list of borrowed books."""
return [book.title for book in self.borrowed_books]

class Library:
"""Library class."""

def __init__(self, name):
"""Initialize library."""
self.name = name
self.books = []
self.members = []

def add_book(self, book):
"""Add a book to the library."""
self.books.append(book)
return f"Added {book.title} to the library"

def add_member(self, member):
"""Add a member to the library."""
self.members.append(member)
return f"Added {member.name} as a member"

def search_books(self, query):
"""Search for books by title or author."""
results = []
query_lower = query.lower()
for book in self.books:
if (query_lower in book.title.lower() or
query_lower in book.author.lower()):
results.append(book)
return results

def get_available_books(self):
"""Get all available books."""
return [book for book in self.books if book.is_available()]

# Using the library system
library = Library("Central Library")

# Add books
book1 = Book("Python Programming", "John Doe", "123456789", 3)
book2 = Book("Data Structures", "Jane Smith", "987654321", 2)
library.add_book(book1)
library.add_book(book2)

# Add members
member1 = Member("Alice", "M001")
member2 = Member("Bob", "M002")
library.add_member(member1)
library.add_member(member2)

# Borrow books
print(member1.borrow_book(book1))
print(member1.borrow_book(book2))
print(member2.borrow_book(book1))

# Search books
results = library.search_books("Python")
for book in results:
print(f"Found: {book}")

# Check available books
available = library.get_available_books()
print(f"Available books: {len(available)}")

Example 2: Employee Management System​

class Employee:
"""Base Employee class."""

def __init__(self, name, employee_id, base_salary):
"""Initialize employee."""
self.name = name
self.employee_id = employee_id
self.base_salary = base_salary

def calculate_salary(self):
"""Calculate salary (to be overridden)."""
return self.base_salary

def get_info(self):
"""Get employee information."""
return f"ID: {self.employee_id}, Name: {self.name}, Salary: ${self.calculate_salary():.2f}"

class Manager(Employee):
"""Manager class."""

def __init__(self, name, employee_id, base_salary, bonus=0):
"""Initialize manager."""
super().__init__(name, employee_id, base_salary)
self.bonus = bonus
self.team = []

def calculate_salary(self):
"""Calculate manager salary with bonus."""
return self.base_salary + self.bonus

def add_team_member(self, employee):
"""Add team member."""
self.team.append(employee)
return f"Added {employee.name} to {self.name}'s team"

def get_team_size(self):
"""Get team size."""
return len(self.team)

class Developer(Employee):
"""Developer class."""

def __init__(self, name, employee_id, base_salary, programming_language):
"""Initialize developer."""
super().__init__(name, employee_id, base_salary)
self.programming_language = programming_language
self.projects = []

def add_project(self, project):
"""Add project."""
self.projects.append(project)
return f"Added {project} to {self.name}'s projects"

def get_info(self):
"""Get developer information."""
base_info = super().get_info()
return f"{base_info}, Language: {self.programming_language}"

class Salesperson(Employee):
"""Salesperson class."""

def __init__(self, name, employee_id, base_salary, commission_rate=0.05):
"""Initialize salesperson."""
super().__init__(name, employee_id, base_salary)
self.commission_rate = commission_rate
self.sales = 0

def add_sale(self, amount):
"""Add a sale."""
self.sales += amount
return f"Added ${amount} sale for {self.name}"

def calculate_salary(self):
"""Calculate salesperson salary with commission."""
commission = self.sales * self.commission_rate
return self.base_salary + commission

# Using the employee system
employees = [
Manager("Alice Johnson", "M001", 80000, 10000),
Developer("Bob Smith", "D001", 70000, "Python"),
Developer("Charlie Brown", "D002", 75000, "JavaScript"),
Salesperson("Diana Prince", "S001", 50000, 0.08)
]

# Manager operations
manager = employees[0]
manager.add_team_member(employees[1])
manager.add_team_member(employees[2])
print(f"Team size: {manager.get_team_size()}")

# Developer operations
developer = employees[1]
developer.add_project("Web Application")
developer.add_project("Mobile App")

# Salesperson operations
salesperson = employees[3]
salesperson.add_sale(10000)
salesperson.add_sale(15000)

# Display all employees
for employee in employees:
print(employee.get_info())

Best Practices for OOP​

Design Principles​

# Single Responsibility Principle
class User:
"""User class with single responsibility."""

def __init__(self, name, email):
self.name = name
self.email = email

class EmailValidator:
"""Email validation with single responsibility."""

@staticmethod
def is_valid(email):
return "@" in email and "." in email

# Open/Closed Principle
class Shape:
"""Open for extension, closed for modification."""

def area(self):
raise NotImplementedError

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

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

# Dependency Inversion Principle
class Database:
"""Abstract database interface."""

def save(self, data):
raise NotImplementedError

class MySQLDatabase(Database):
def save(self, data):
return f"Saved to MySQL: {data}"

class UserService:
"""User service depending on abstraction."""

def __init__(self, database):
self.database = database

def create_user(self, user):
return self.database.save(user.name)

# Using dependency injection
mysql_db = MySQLDatabase()
user_service = UserService(mysql_db)
user = User("Alice", "[email protected]")
result = user_service.create_user(user)
print(result)

Summary​

In this chapter, we've covered:

  • Classes and objects: The foundation of OOP in Python
  • Methods: Instance, class, and static methods
  • Inheritance: Single and multiple inheritance patterns
  • Encapsulation: Data hiding and property decorators
  • Special methods: Magic methods for custom behavior
  • Polymorphism: Method overriding and duck typing
  • Advanced concepts: Abstract base classes and composition
  • Design principles: SOLID principles and best practices

Object-Oriented Programming is a powerful paradigm that helps you create maintainable, reusable, and organized code. Understanding these concepts is essential for building complex applications and working with Python frameworks and libraries.

Next Steps​

Now that you understand Object-Oriented Programming, you're ready to explore:

  1. Advanced Python Topics: Decorators, generators, and context managers
  2. File Handling: Reading from and writing to files
  3. Error Handling: Exception management and debugging
  4. Testing: Unit testing and test-driven development
  5. Package Management: Virtual environments and package distribution

Ready to explore advanced Python features? Continue with Chapter 6: Advanced Python Topics to master Python's advanced concepts!