AiTechWorlds
AiTechWorlds
You've been writing Python programs with variables and functions. That works well — until programs get large. Imagine building a game with 100 characters, each with a name, health, attack power, and abilities. With just variables and functions, you'd have hundreds of separate variables and no clear structure.
There's a better way to organize code: Object-Oriented Programming (OOP).
The central idea is beautifully simple: instead of treating a program as a list of instructions, you think of it as a collection of things — objects that have properties and can do things.
Think about building houses.
You don't build each house from scratch by making new blueprints every time. You create one blueprint (the class), then you build multiple houses from it (the objects). Each house follows the same blueprint but has its own address, color, and furniture.
In Python:
# The BLUEPRINT
class Dog:
def __init__(self, name, breed, age):
self.name = name
self.breed = breed
self.age = age
def bark(self):
print(f"{self.name} says: Woof!")
def describe(self):
print(f"{self.name} is a {self.age}-year-old {self.breed}")
# Creating OBJECTS from the blueprint
dog1 = Dog("Rex", "German Shepherd", 3)
dog2 = Dog("Bella", "Golden Retriever", 5)
dog1.bark()
dog2.describe()
dog1.describe()
Output:
Rex says: Woof!
Bella is a 5-year-old Golden Retriever
Rex is a 3-year-old German Shepherd
Same blueprint (Dog), different objects with different data. Notice how each dog has its own name, breed, and age — but they share the same structure and behaviors.
__init__ Method: Creating Objects__init__ is a special method that runs automatically when you create a new object. It's the constructor — it sets up the object with its initial data.
class Student:
def __init__(self, name, student_id, gpa=0.0):
# self.attribute_name = value
self.name = name # Store name on this object
self.student_id = student_id # Store ID on this object
self.gpa = gpa # Default GPA is 0.0
self.courses = [] # Start with empty course list
When you write Student("Alice", "S001"):
Student object__init__ runs automaticallyself refers to the newly created objectself is Python's way of saying "this specific object." Every method in a class receives self as its first parameter, even though you don't pass it when calling.
Attributes are the data stored inside an object. Two types exist:
Instance attributes — unique to each object:
class Car:
def __init__(self, make, model, year):
self.make = make # Instance attribute — different for each car
self.model = model
self.year = year
self.mileage = 0 # Every new car starts at 0
car1 = Car("Toyota", "Camry", 2023)
car2 = Car("Honda", "Civic", 2022)
print(car1.make) # Toyota
print(car2.make) # Honda
Class attributes — shared by ALL objects of that class:
class BankAccount:
bank_name = "AiTech Bank" # Class attribute — same for all accounts
interest_rate = 0.04 # Same for all
def __init__(self, owner, balance):
self.owner = owner # Instance attribute — unique
self.balance = balance
acc1 = BankAccount("Alice", 1000)
acc2 = BankAccount("Bob", 5000)
print(acc1.bank_name) # AiTech Bank
print(acc2.bank_name) # AiTech Bank (shared!)
print(acc1.balance) # 1000
print(acc2.balance) # 5000 (unique!)
Methods are functions defined inside a class. They always take self as the first argument:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
print("Amount must be positive")
return
self.balance += amount
self.transactions.append(f"+ ${amount:.2f}")
print(f"Deposited ${amount:.2f}. Balance: ${self.balance:.2f}")
def withdraw(self, amount):
if amount <= 0:
print("Amount must be positive")
elif amount > self.balance:
print(f"Insufficient funds. Balance: ${self.balance:.2f}")
else:
self.balance -= amount
self.transactions.append(f"- ${amount:.2f}")
print(f"Withdrew ${amount:.2f}. Balance: ${self.balance:.2f}")
def get_balance(self):
return self.balance
def print_history(self):
print(f"\n{'='*35}")
print(f"Account: {self.owner}")
print(f"{'='*35}")
if not self.transactions:
print(" No transactions yet")
for t in self.transactions:
print(f" {t}")
print(f" {'─'*29}")
print(f" Current Balance: ${self.balance:.2f}")
# Using the class
alice = BankAccount("Alice", 500)
bob = BankAccount("Bob")
alice.deposit(250)
alice.withdraw(100)
alice.withdraw(1000) # Should fail
bob.deposit(750)
alice.print_history()
bob.print_history()
Output:
Deposited $250.00. Balance: $750.00
Withdrew $100.00. Balance: $650.00
Insufficient funds. Balance: $650.00
Deposited $750.00. Balance: $750.00
===================================
Account: Alice
===================================
+ $250.00
- $100.00
─────────────────────────────
Current Balance: $650.00
===================================
Account: Bob
===================================
+ $750.00
─────────────────────────────
Current Balance: $750.00
| Approach | Without Classes (Procedural) | With Classes (OOP) |
|---|---|---|
| Data | Separate variables for each item | Grouped inside objects |
| Organization | Functions and loose variables | Self-contained objects |
| Scaling | Gets messy fast | Stays organized |
| Reuse | Copy-paste code | Create new instances |
| Real-world modeling | Awkward | Natural |
For a program managing 1 item, procedural is fine. For 100 items with shared behavior, OOP wins.
A class should represent a single, clear concept from the real world or your problem domain:
✅ Good class names: Student, Product, BankAccount, Employee, Order
❌ Bad class names: DataProcessor, Manager, Handler (too vague)
A class should:
Design a Book class that has:
Think about what happens if someone tries to check out a book that's already checked out. How would you handle that?
In the next lesson, we'll add more advanced features to our classes with special methods and better encapsulation.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises