Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
22 minLesson 11 of 34
Object-Oriented Python

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

Get Notes Free →
!