Classes and Objects
Classes and Objects: Python's Object-Oriented System
Object-oriented programming (OOP) organizes code around objects — bundles of data and behavior that model real-world concepts. Classes define what an object is; objects are instances of a class. This is how most production Python code is structured.
Why Classes?
Without classes, related data and functions live separately and things get messy:
# Without classes — brittle, scattered
user_name = "Alice"
user_age = 30
user_email = "alice@example.com"
def greet_user(name):
return f"Hello, {name}!"
def get_user_info(name, age, email):
return f"{name}, {age}, {email}"
# With classes — organized, self-contained
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def greet(self):
return f"Hello, {self.name}!"
def get_info(self):
return f"{self.name}, {self.age}, {self.email}"
alice = User("Alice", 30, "alice@example.com")
print(alice.greet()) # Hello, Alice!
print(alice.get_info()) # Alice, 30, alice@example.com
Anatomy of a Class
class BankAccount:
# Class attribute: shared by ALL instances
bank_name = "Python Bank"
interest_rate = 0.02
# __init__: called when creating an instance
def __init__(self, owner, balance=0):
# Instance attributes: unique to each instance
self.owner = owner
self.balance = balance
self._transaction_history = [] # Convention: _ means "private"
# Instance method: operates on the instance via self
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
self._transaction_history.append(f"Deposit: +{amount}")
return self.balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError(f"Insufficient funds: balance is {self.balance}")
self.balance -= amount
self._transaction_history.append(f"Withdrawal: -{amount}")
return self.balance
def apply_interest(self):
interest = round(self.balance * self.interest_rate, 2)
self.balance += interest
self._transaction_history.append(f"Interest: +{interest}")
return interest
def get_statement(self):
lines = [f"Account: {self.owner}", f"Bank: {self.bank_name}"]
lines.extend(self._transaction_history)
lines.append(f"Balance: ${self.balance:.2f}")
return "\n".join(lines)
# __str__: what print() shows
def __str__(self):
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
# __repr__: unambiguous string representation (for debugging)
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self.balance!r})"
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
account.apply_interest()
print(account.get_statement())
print(account) # Calls __str__
print(repr(account)) # Calls __repr__
# Accessing class attributes
print(BankAccount.bank_name) # Via class
print(account.bank_name) # Via instance (inherits from class)
Class Methods and Static Methods
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
"""Property: accessed like an attribute, not a method call."""
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Alternative constructor — a class method creates instances."""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius) # cls is the class itself
@classmethod
def from_kelvin(cls, kelvin):
return cls(kelvin - 273.15)
@staticmethod
def celsius_to_fahrenheit(celsius):
"""Static method: utility function that doesn't need instance or class."""
return celsius * 9/5 + 32
def __str__(self):
return f"{self.celsius:.1f}°C / {self.fahrenheit:.1f}°F"
# Usage
boiling = Temperature(100)
print(boiling) # 100.0°C / 212.0°F
# Alternative constructors
freezing = Temperature.from_fahrenheit(32)
print(freezing) # 0.0°C / 32.0°F
body_temp = Temperature.from_kelvin(310)
print(body_temp) # 36.9°C / 98.3°F
# Property setter
t = Temperature(20)
t.fahrenheit = 68 # Sets via property setter
print(t.celsius) # 20.0
# Static method — no instance needed
print(Temperature.celsius_to_fahrenheit(37)) # 98.6
Dataclasses: Modern Python Classes
For classes that mainly hold data, @dataclass removes boilerplate:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Student:
name: str
grade: int
scores: List[float] = field(default_factory=list)
@property
def average(self):
return sum(self.scores) / len(self.scores) if self.scores else 0
def add_score(self, score):
self.scores.append(score)
def __post_init__(self):
"""Called after __init__ — validate or transform data."""
if not 1 <= self.grade <= 12:
raise ValueError(f"Grade must be 1-12, got {self.grade}")
self.name = self.name.strip().title()
# Dataclasses automatically generate __init__, __repr__, __eq__
s = Student("alice smith", 10, [88, 92, 76])
print(s) # Student(name='Alice Smith', grade=10, scores=[88, 92, 76])
print(s.average) # 85.33
print(s == Student("Alice Smith", 10, [88, 92, 76])) # True
A Real-World Example: Product Inventory
from dataclasses import dataclass, field
from typing import Dict, Optional
from datetime import datetime
@dataclass
class Product:
sku: str
name: str
price: float
category: str
def discounted_price(self, discount_pct):
return round(self.price * (1 - discount_pct / 100), 2)
def __str__(self):
return f"{self.name} (${self.price:.2f})"
class Inventory:
def __init__(self):
self._stock: Dict[str, int] = {}
self._products: Dict[str, Product] = {}
self._log = []
def add_product(self, product: Product, quantity: int):
self._products[product.sku] = product
self._stock[product.sku] = self._stock.get(product.sku, 0) + quantity
self._log.append(f"{datetime.now()}: Added {quantity}× {product.name}")
def sell(self, sku: str, quantity: int) -> float:
if sku not in self._products:
raise ValueError(f"Unknown SKU: {sku}")
if self._stock[sku] < quantity:
raise ValueError(f"Only {self._stock[sku]} in stock")
product = self._products[sku]
self._stock[sku] -= quantity
revenue = product.price * quantity
self._log.append(f"{datetime.now()}: Sold {quantity}× {product.name}")
return revenue
def low_stock(self, threshold=10) -> list:
return [
(self._products[sku], qty)
for sku, qty in self._stock.items()
if qty <= threshold
]
def total_value(self) -> float:
return sum(
self._products[sku].price * qty
for sku, qty in self._stock.items()
)
def __len__(self):
return sum(self._stock.values())
def __contains__(self, sku):
return sku in self._products
# Usage
inv = Inventory()
laptop = Product("LAP001", "MacBook Pro", 2499.99, "Electronics")
mouse = Product("MOU001", "Magic Mouse", 79.99, "Electronics")
inv.add_product(laptop, 5)
inv.add_product(mouse, 50)
revenue = inv.sell("LAP001", 2)
print(f"Revenue: ${revenue:.2f}")
print(f"Total inventory value: ${inv.total_value():,.2f}")
print(f"Total items in stock: {len(inv)}")
print(f"'LAP001' in inventory: {'LAP001' in inv}")
Classes are the backbone of organized Python code. Once you understand them intuitively, you'll see that almost every library you use — pandas DataFrames, scikit-learn models, Django views — is built from classes.
Next lesson: Inheritance & Polymorphism — extending and reusing classes.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises