05.1 - Classes & Objects
What is Object-Oriented Programming?
OOP organizes code around objects — entities that bundle data (attributes) and behavior (methods) together.
The 4 pillars of OOP:
| Pillar | Definition |
|---|---|
| Encapsulation | Bundle data and methods; hide internal state |
| Inheritance | Create new classes based on existing ones |
| Polymorphism | Different classes respond to the same interface |
| Abstraction | Expose only what is necessary |
Defining a Class
class BankAccount:
"""Represents a bank account with balance management."""
# Class variable — shared by ALL instances
bank_name = "Python National Bank"
_account_count = 0
def __init__(self, owner: str, initial_balance: float = 0.0):
"""Initialize the account."""
# Instance variables — unique to each instance
self.owner = owner
self._balance = initial_balance # "private" by convention
self._transactions = []
BankAccount._account_count += 1
def deposit(self, amount: float) -> float:
"""Add money to the account."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transactions.append(f"+{amount:.2f}")
return self._balance
def withdraw(self, amount: float) -> float:
"""Remove money from the account."""
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self._balance:
raise ValueError(f"Insufficient funds (balance: {self._balance:.2f})")
self._balance -= amount
self._transactions.append(f"-{amount:.2f}")
return self._balance
def get_balance(self) -> float:
"""Return current balance."""
return self._balance
def get_history(self) -> list:
"""Return transaction history."""
return list(self._transactions)
@classmethod
def get_account_count(cls) -> int:
"""Return total number of accounts created."""
return cls._account_count
@staticmethod
def is_valid_amount(amount) -> bool:
"""Check if an amount is a valid positive number."""
return isinstance(amount, (int, float)) and amount > 0
def __repr__(self) -> str:
return f"BankAccount(owner='{self.owner}', balance={self._balance:.2f})"
def __str__(self) -> str:
return f"{self.owner}'s account — ${self._balance:,.2f}"
Creating and Using Objects
# Creating instances
alice_account = BankAccount("Alice", 1000.0)
bob_account = BankAccount("Bob")
# Using methods
alice_account.deposit(500)
alice_account.withdraw(200)
print(alice_account) # Alice's account — $1,300.00
print(repr(alice_account)) # BankAccount(owner='Alice', balance=1300.00)
# Class method
BankAccount.get_account_count() # 2
# Static method
BankAccount.is_valid_amount(100) # True
BankAccount.is_valid_amount(-5) # False
Instance vs Class vs Static Methods
┌─────────────────────────────────────────────────────────────┐
│ Method Types │
│ │
│ Instance method: def f(self, ...) → accesses instance │
│ Class method: @classmethod │
│ def f(cls, ...) → accesses class │
│ Static method: @staticmethod │
│ def f(...) → no self or cls │
└─────────────────────────────────────────────────────────────┘
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
# Instance method
def to_fahrenheit(self):
return (self.celsius * 9/5) + 32
# Class method — alternative constructor
@classmethod
def from_fahrenheit(cls, fahrenheit):
return cls((fahrenheit - 32) * 5/9)
# Static method — utility, no state needed
@staticmethod
def is_freezing(celsius):
return celsius <= 0
t = Temperature(100)
t.to_fahrenheit() # 212.0
t2 = Temperature.from_fahrenheit(32)
t2.celsius # 0.0
Temperature.is_freezing(-5) # True
Properties: Controlled Attribute Access
@property creates a getter; @x.setter creates a setter with validation:
class Circle:
def __init__(self, radius):
self._radius = radius # store in private attribute
@property
def radius(self):
"""Get radius (always positive)."""
return self._radius
@radius.setter
def radius(self, value):
"""Set radius with validation."""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Computed property — no setter needed."""
import math
return math.pi * self._radius ** 2
@property
def diameter(self):
return self._radius * 2
c = Circle(5)
c.radius # 5
c.area # 78.54...
c.radius = 10 # uses setter
c.radius = -1 # ValueError!
Dataclasses (Python 3.7+)
For simple data-holding classes, @dataclass eliminates boilerplate:
from dataclasses import dataclass, field
from typing import ClassVar
@dataclass
class Product:
name: str
price: float
quantity: int = 0
tags: list = field(default_factory=list)
_discount: float = field(default=0.0, repr=False)
# Class variable
currency: ClassVar[str] = "CAD"
@property
def total_value(self):
return self.price * self.quantity
def apply_discount(self, percent):
self._discount = percent / 100
self.price *= (1 - self._discount)
p = Product("Laptop", 999.99, 5)
print(p) # Product(name='Laptop', price=999.99, quantity=5, tags=[])
p.total_value # 4999.95
Key Vocabulary
| Term | Definition |
|---|---|
| Class | Blueprint for creating objects |
| Object / Instance | A specific realization of a class |
__init__ | Constructor — called when creating an instance |
self | Reference to the current instance |
| Instance variable | Attribute unique to each object: self.x |
| Class variable | Attribute shared by all instances |
@classmethod | Method that receives the class (cls) as first argument |
@staticmethod | Method with no access to instance or class |
@property | Turns a method into a read-only attribute accessor |
@dataclass | Decorator that auto-generates __init__, __repr__, __eq__ |
Summary
- Classes are blueprints; objects are instances created from them
__init__sets up instance state usingself.attribute = value- Instance methods take
self; class methods takecls; static methods take neither - Use
_namefor protected and__namefor name-mangled (private) attributes @propertyprovides controlled access to attributes with validation@dataclassauto-generates boilerplate for data-holding classes
📄️ 05.1 - Classes & Objects
Define Python classes with __init__, instance methods, class variables, static methods, and properties
📄️ 05.2 - Inheritance & Polymorphism
Design class hierarchies using inheritance, method overriding, super(), abstract classes, and polymorphism
📄️ 05.3 - Dunder Methods
Control object behavior with Python's special methods: __repr__, __eq__, __len__, __add__, __iter__, and more
📄️ Lab - Module 05
Build a complete OOP bank account system with inheritance, properties, dunder methods, and abstract classes
📄️ Quiz - Module 05
30 questions on classes, inheritance, polymorphism, and dunder methods