What is Object-Oriented Programming?
Object-Oriented Programming (OOP) organizes code around objects — bundles of data and behavior that model real-world things. A class is the blueprint; an object is an instance built from that blueprint.
Think of it like this: "Dog" is a class (the concept), while "Rex the golden retriever" is an object (a specific dog).
Creating Your First Class
class Dog:
def __init__(self, name, breed):
self.name = name # instance attribute
self.breed = breed # instance attribute
def bark(self): # method
print(f"{self.name} says: Woof!")
def info(self): # method
print(f"{self.name} is a {self.breed}")
# Create objects (instances)
rex = Dog("Rex", "Golden Retriever")
luna = Dog("Luna", "German Shepherd")
rex.bark() # Rex says: Woof!
luna.info() # Luna is a German Shepherd
self?
self refers to the specific object calling the method. When you
write rex.bark(), Python passes rex as self
automatically. Every method must have self as its first parameter.
The __init__ Constructor
__init__ runs automatically when you create a new object. Use it to
set up the object's initial state:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount > 0:
self.balance += amount
self.transactions.append(f"+${amount}")
print(f"Deposited ${amount}. Balance: ${self.balance}")
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds!")
return
self.balance -= amount
self.transactions.append(f"-${amount}")
print(f"Withdrew ${amount}. Balance: ${self.balance}")
def statement(self):
print(f"\nAccount: {self.owner}")
print(f"Balance: ${self.balance}")
print(f"Transactions: {', '.join(self.transactions)}")
# Usage
account = BankAccount("Alice", 1000)
account.deposit(500) # Deposited $500. Balance: $1500
account.withdraw(200) # Withdrew $200. Balance: $1300
account.statement()
Instance vs Class Attributes
class Car:
# Class attribute — shared by ALL instances
wheels = 4
def __init__(self, make, model, year):
# Instance attributes — unique to each object
self.make = make
self.model = model
self.year = year
self.mileage = 0
def drive(self, km):
self.mileage += km
car1 = Car("Toyota", "Camry", 2024)
car2 = Car("Honda", "Civic", 2023)
print(car1.wheels) # 4 (from class)
print(car2.wheels) # 4 (from class)
print(Car.wheels) # 4 (access via class)
car1.drive(100)
print(car1.mileage) # 100
print(car2.mileage) # 0 (each has its own)
Special Methods (Dunder Methods)
Python uses double-underscore methods (dunder methods) to define how objects behave with built-in operations:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
"""Called by print() and str()"""
return f"{self.name}: ${self.price:.2f}"
def __repr__(self):
"""Called in debugger and interactive shell"""
return f"Product('{self.name}', {self.price})"
def __eq__(self, other):
"""Called by == operator"""
return self.name == other.name and self.price == other.price
def __lt__(self, other):
"""Called by < operator (enables sorting)"""
return self.price < other.price
items = [
Product("Laptop", 999.99),
Product("Mouse", 29.99),
Product("Keyboard", 79.99)
]
print(items[0]) # Laptop: $999.99
items.sort() # Sorts by price (uses __lt__)
for item in items:
print(item) # Mouse, Keyboard, Laptop
Encapsulation
Encapsulation means hiding internal details and controlling access to data:
class User:
def __init__(self, username, password):
self.username = username
self._password = password # Convention: "private" (one underscore)
def check_password(self, attempt):
return attempt == self._password
def change_password(self, old, new):
if self.check_password(old):
self._password = new
print("Password changed!")
else:
print("Wrong password!")
user = User("alice", "secret123")
user.check_password("secret123") # True
user.change_password("secret123", "newpass456")
A single underscore (_name) signals "don't access this directly."
A double underscore (__name) triggers name mangling for stronger
protection. Neither truly prevents access — Python trusts developers.
Practical Example: Task Manager
class Task:
def __init__(self, title, priority="medium"):
self.title = title
self.priority = priority
self.completed = False
def complete(self):
self.completed = True
def __str__(self):
status = "done" if self.completed else "pending"
return f"[{status}] {self.title} ({self.priority})"
class TaskManager:
def __init__(self):
self.tasks = []
def add(self, title, priority="medium"):
task = Task(title, priority)
self.tasks.append(task)
print(f"Added: {title}")
def complete(self, title):
for task in self.tasks:
if task.title == title:
task.complete()
print(f"Completed: {title}")
return
print(f"Task not found: {title}")
def show(self, show_completed=True):
for task in self.tasks:
if show_completed or not task.completed:
print(f" {task}")
def pending_count(self):
return sum(1 for t in self.tasks if not t.completed)
# Usage
mgr = TaskManager()
mgr.add("Write report", "high")
mgr.add("Buy groceries", "low")
mgr.add("Fix bug #42", "high")
mgr.complete("Write report")
print(f"\nAll tasks:")
mgr.show()
print(f"\nPending: {mgr.pending_count()}")
Summary
- Classes are blueprints; objects are instances created from classes
__init__initializes new objects;selfrefers to the current instance- Instance attributes belong to each object; class attributes are shared
- Dunder methods (
__str__,__eq__, etc.) customize built-in behavior - Use underscore prefix (
_name) to signal private attributes - Classes bundle related data and behavior together, making code organized and reusable
You can now create your own classes and objects. Next up: inheritance and polymorphism — building class hierarchies and sharing code between related classes.