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.