Skip to main content

Module 10 - Object-Oriented Programming Basics

This module covers Object-Oriented Programming (OOP) basics in Python. You'll learn how to create classes, objects, attributes, and methods—the foundation of OOP in Python.


1. What is Object-Oriented Programming?

1.1 OOP Concepts

OOP is a programming paradigm that organizes code around objects (data + behavior) rather than functions and logic.

Key Principles:

  • Encapsulation: Bundle data and methods together
  • Abstraction: Hide complex implementation details
  • Inheritance: Create new classes from existing ones
  • Polymorphism: Same interface, different implementations
Why OOP?

OOP makes code more modular, reusable, and easier to maintain. It models real-world entities naturally.


2. Classes and Objects

2.1 Creating a Class

# Define a class
class Dog:
pass

# Create an object (instance)
my_dog = Dog()
print(type(my_dog)) # <class '__main__.Dog'>

2.2 Adding Attributes

# Class with attributes
class Dog:
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age

# Create instances
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

print(dog1.name) # Buddy
print(dog2.age) # 5

2.3 The init Method

class Person:
def __init__(self, name, age):
"""Constructor method"""
self.name = name
self.age = age
print(f"Person created: {name}")

person = Person("Alice", 25)
# Output: Person created: Alice
The self Parameter

self refers to the instance itself. It's always the first parameter of instance methods, but you don't pass it explicitly when calling methods.


3. Instance Methods

3.1 Defining Methods

class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

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

def get_age_in_human_years(self):
return self.age * 7

dog = Dog("Buddy", 3)
print(dog.bark()) # Buddy says Woof!
print(dog.get_age_in_human_years()) # 21

3.2 Methods with Parameters

class Calculator:
def __init__(self):
self.result = 0

def add(self, value):
self.result += value
return self

def subtract(self, value):
self.result -= value
return self

def get_result(self):
return self.result

calc = Calculator()
result = calc.add(10).subtract(3).get_result()
print(result) # 7

4. Instance vs Class Attributes

4.1 Instance Attributes

class Student:
def __init__(self, name, grade):
self.name = name # Instance attribute
self.grade = grade # Instance attribute

student1 = Student("Alice", "A")
student2 = Student("Bob", "B")

print(student1.name) # Alice
print(student2.name) # Bob

4.2 Class Attributes

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

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

dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(Dog.species) # Canis familiaris

# Modifying class attribute
Dog.species = "Modified species"
print(dog1.species) # Modified species
print(dog2.species) # Modified species

4.3 Instance vs Class Comparison

class Counter:
count = 0 # Class attribute

def __init__(self, name):
self.name = name # Instance attribute
Counter.count += 1 # Increment class attribute
self.instance_id = Counter.count # Instance-specific ID

c1 = Counter("First")
c2 = Counter("Second")
c3 = Counter("Third")

print(Counter.count) # 3
print(c1.instance_id) # 1
print(c2.instance_id) # 2
print(c3.instance_id) # 3

5. Class Methods and Static Methods

5.1 Class Methods

class Employee:
raise_amount = 1.05 # Class attribute

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

@classmethod
def set_raise_amount(cls, amount):
"""Modify class attribute"""
cls.raise_amount = amount

@classmethod
def from_string(cls, emp_str):
"""Alternative constructor"""
name, salary = emp_str.split('-')
return cls(name, int(salary))

def apply_raise(self):
self.salary = int(self.salary * self.raise_amount)

# Using class method
Employee.set_raise_amount(1.10)

# Alternative constructor
emp1 = Employee.from_string("Alice-50000")
print(emp1.name, emp1.salary) # Alice 50000

5.2 Static Methods

class MathOperations:
@staticmethod
def add(a, b):
"""Static method - doesn't access instance or class"""
return a + b

@staticmethod
def is_even(number):
return number % 2 == 0

# Call without creating instance
print(MathOperations.add(5, 3)) # 8
print(MathOperations.is_even(10)) # True

5.3 When to Use Each

Method TypeAccessUse Case
Instance methodself (instance)Operations on instance data
Class methodcls (class)Factory methods, modify class state
Static methodNeitherUtility functions related to class

6. Properties and Encapsulation

6.1 Public vs Private Attributes

class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._balance = balance # Protected (convention)
self.__pin = 1234 # Private (name mangling)

def get_balance(self):
return self._balance

def deposit(self, amount):
if amount > 0:
self._balance += amount

account = BankAccount("Alice", 1000)
print(account.owner) # Alice (public)
print(account._balance) # 1000 (accessible but discouraged)
# print(account.__pin) # AttributeError
print(account._BankAccount__pin) # 1234 (name mangling workaround)
Python Conventions
  • Single underscore _attr: Protected (internal use)
  • Double underscore __attr: Private (name mangling)
  • No underscore attr: Public

These are conventions, not strict access control!

6.2 Property Decorator

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

@property
def radius(self):
"""Getter"""
return self._radius

@radius.setter
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def area(self):
"""Computed property"""
return 3.14159 * self._radius ** 2

circle = Circle(5)
print(circle.radius) # 5 (calls getter)
print(circle.area) # 78.53975

circle.radius = 10 # Calls setter
print(circle.area) # 314.159

# circle.radius = -5 # ValueError

6.3 Getters and Setters

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

@property
def celsius(self):
return self._celsius

@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value

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

@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9

temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0

temp.fahrenheit = 32
print(temp.celsius) # 0.0

7. String Representation

7.1 str and repr

class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year

def __str__(self):
"""User-friendly string"""
return f"{self.title} by {self.author} ({self.year})"

def __repr__(self):
"""Developer-friendly representation"""
return f"Book('{self.title}', '{self.author}', {self.year})"

book = Book("1984", "George Orwell", 1949)

print(str(book)) # 1984 by George Orwell (1949)
print(repr(book)) # Book('1984', 'George Orwell', 1949)
print(book) # Uses __str__
str vs repr
  • __str__: Human-readable, for end users
  • __repr__: Unambiguous, for developers/debugging
  • If only __repr__ is defined, it's used for both

8. Object Comparison

8.1 Equality Methods

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

def __eq__(self, other):
"""Equal to"""
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False

def __ne__(self, other):
"""Not equal to"""
return not self.__eq__(other)

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(2, 3)

print(p1 == p2) # True
print(p1 == p3) # False
print(p1 != p3) # True

8.2 Ordering Methods

class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade

def __lt__(self, other):
"""Less than"""
return self.grade < other.grade

def __le__(self, other):
"""Less than or equal"""
return self.grade <= other.grade

def __gt__(self, other):
"""Greater than"""
return self.grade > other.grade

def __ge__(self, other):
"""Greater than or equal"""
return self.grade >= other.grade

students = [
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78)
]

sorted_students = sorted(students)
for s in sorted_students:
print(s.name, s.grade)
# Charlie 78
# Alice 85
# Bob 92

9. Real-World Examples

9.1 Bank Account Class

class BankAccount:
"""A simple bank account implementation."""

interest_rate = 0.02 # Class attribute

def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
self._transactions = []

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

def deposit(self, amount):
if amount > 0:
self._balance += amount
self._transactions.append(f"Deposit: +${amount}")
return True
return False

def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
self._transactions.append(f"Withdraw: -${amount}")
return True
return False

def apply_interest(self):
interest = self._balance * self.interest_rate
self._balance += interest
self._transactions.append(f"Interest: +${interest:.2f}")

def get_statement(self):
statement = f"Account: {self.owner}\n"
statement += f"Balance: ${self._balance:.2f}\n"
statement += "Transactions:\n"
for transaction in self._transactions:
statement += f" {transaction}\n"
return statement

def __str__(self):
return f"BankAccount({self.owner}, ${self._balance:.2f})"

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
account.apply_interest()
print(account.get_statement())

9.2 Shopping Cart Class

class Product:
def __init__(self, name, price):
self.name = name
self.price = price

def __str__(self):
return f"{self.name}: ${self.price:.2f}"

class ShoppingCart:
def __init__(self):
self.items = []

def add_item(self, product, quantity=1):
self.items.append({"product": product, "quantity": quantity})

def remove_item(self, product_name):
self.items = [item for item in self.items
if item["product"].name != product_name]

def get_total(self):
return sum(item["product"].price * item["quantity"]
for item in self.items)

def display_cart(self):
print("Shopping Cart:")
for item in self.items:
product = item["product"]
quantity = item["quantity"]
subtotal = product.price * quantity
print(f" {product.name} x{quantity} = ${subtotal:.2f}")
print(f"Total: ${self.get_total():.2f}")

# Usage
cart = ShoppingCart()
cart.add_item(Product("Apple", 0.50), 5)
cart.add_item(Product("Bread", 2.50), 2)
cart.add_item(Product("Milk", 3.99), 1)
cart.display_cart()

10. Best Practices

10.1 Class Design Principles

# ✅ Single Responsibility
class User:
"""Handles user data only"""
def __init__(self, username, email):
self.username = username
self.email = email

class UserRepository:
"""Handles user persistence"""
def save(self, user):
pass

def find_by_username(self, username):
pass

# ❌ Too many responsibilities
class User:
def __init__(self, username):
self.username = username

def save_to_database(self): # Database logic
pass

def send_email(self): # Email logic
pass

def generate_report(self): # Reporting logic
pass

10.2 Naming Conventions

# Class names: PascalCase
class BankAccount:
pass

class ShoppingCart:
pass

# Method names: snake_case
def get_balance(self):
pass

def calculate_total(self):
pass

# Private attributes: leading underscore
self._internal_value = 10
self.__very_private = 20

11. Summary

ConceptDescriptionExample
ClassBlueprint for objectsclass Dog:
ObjectInstance of a classdog = Dog()
initConstructor methoddef __init__(self):
selfReference to instanceself.name = name
Instance attributeUnique to each objectself.name
Class attributeShared by all instancesDog.species
@propertyGetter/setter decorator@property def name():
@classmethodMethod that receives class@classmethod def create():
@staticmethodIndependent method@staticmethod def util():
Key Takeaways
  • Classes group data and behavior together
  • Use __init__ to initialize objects
  • Instance attributes are unique per object
  • Class attributes are shared across instances
  • Use properties for controlled attribute access
  • Follow naming conventions and design principles
  • Keep classes focused (Single Responsibility)

12. What's Next?

In Module 11 - OOP Advanced, you'll learn:

  • Inheritance and method overriding
  • Multiple inheritance
  • Polymorphism
  • Abstract base classes
  • Method Resolution Order (MRO)

13. Practice Exercises

Exercise 1: Create a Rectangle Class

Implement a Rectangle class with width, height, area, and perimeter calculations.

Exercise 2: Student Management System

Create Student and Course classes with enrollment functionality.

Exercise 3: Library System

Build Book and Library classes with checkout/return functionality.

Exercise 4: Temperature Converter

Create a Temperature class with Celsius, Fahrenheit, and Kelvin conversions.

Exercise 5: Banking System

Expand the BankAccount class with transfer, transaction history, and overdraft protection.

Solutions

Try solving these exercises on your own first. Solutions will be provided in the practice section.