1. What Is Object-Oriented Programming?
Imagine an architect drawing blueprints for a house. The blueprint isn't a house — it's a plan that describes exactly what a house built from it will look like: how many rooms, where the doors go, how the wiring runs. Once the blueprint exists, you can build any number of houses from it. Each house is real and independent — you can paint one green without affecting the others.
In Python, a class is the blueprint. An object is a house built from that blueprint. You can create as many objects from one class as you want, and each one has its own independent data.
Class = the blueprint (defined once). Object = a specific instance built from that blueprint (can have many). Each object has its own data, but they all share the same structure and capabilities defined by the class.
Why Use OOP?
- Organise complexity — bundle related data and functions together instead of scattering them across your code
- Reuse code — write a class once, use it in many places without copy-pasting
- Model the real world — your code mirrors how you think about the problem
- Collaborate better — in a team, each developer can own a class without breaking others' work
Real examples: a Student class (each student has a name, branch, and marks), a Robot class (each robot has a speed, direction, and sensor reading), a BankAccount class (each account has a balance and a transaction history). Django — the web framework that powers Instagram — is built entirely from classes.
2. Classes and Objects
Let's build our first class. The class keyword defines a new blueprint. By convention, class names start with a capital letter.
The __init__ Method
Every class has a special method called __init__. This is the constructor — it runs automatically every time you create a new object from the class. Think of it as the setup instructions that run the moment a house is built from the blueprint.
The self Parameter
self is Python's way of saying "I am referring to this specific object." When you have ten Student objects and you call a method on one of them, self makes sure the method works on that particular student's data, not everyone else's. You always write it as the first parameter of every method, but you never pass it manually — Python handles that.
class Student:
def __init__(self, name, branch, marks):
# These are attributes — data belonging to this object
self.name = name
self.branch = branch
self.marks = marks
def get_grade(self):
"""Return the letter grade based on marks."""
if self.marks >= 90:
return 'A+'
elif self.marks >= 80:
return 'A'
elif self.marks >= 70:
return 'B'
elif self.marks >= 60:
return 'C'
else:
return 'F'
def introduce(self):
grade = self.get_grade()
print(f"Hi, I'm {self.name} from {self.branch}. "
f"I scored {self.marks} — Grade {grade}.")
# Creating objects (instances of Student)
s1 = Student("Aisha", "ECE", 88)
s2 = Student("Dev", "Mechanical", 74)
s1.introduce() # Hi, I'm Aisha from ECE. I scored 88 — Grade A.
s2.introduce() # Hi, I'm Dev from Mechanical. I scored 74 — Grade B.
# Each object has its own data
print(s1.name) # Aisha
print(s2.name) # Dev
Attributes are the data an object holds — like self.name and self.marks. Think of them as the properties of the object.
Methods are the functions the object can perform — like get_grade() and introduce(). Think of them as the object's actions.
3. Methods — What Objects Can Do
Methods are just functions that belong to a class. They always have self as their first parameter, giving them access to the object's own data.
The __str__ Method
Python calls __str__ automatically whenever you try to print an object. Without it, printing a Student object shows something like <Student object at 0x7f...> — not very helpful. Define __str__ and printing your object becomes meaningful.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = [] # list of all transactions
def deposit(self, amount):
if amount <= 0:
print("Deposit amount must be positive.")
return
self.balance += amount
self.transactions.append(f"+{amount}")
print(f"Deposited ₹{amount}. New balance: ₹{self.balance}")
def withdraw(self, amount):
if amount <= 0:
print("Withdrawal amount must be positive.")
return
if amount > self.balance:
print(f"Insufficient funds. Balance: ₹{self.balance}")
return
self.balance -= amount
self.transactions.append(f"-{amount}")
print(f"Withdrew ₹{amount}. New balance: ₹{self.balance}")
def get_statement(self):
print(f"\n--- Account: {self.owner} ---")
for t in self.transactions:
print(f" {t}")
print(f" Balance: ₹{self.balance}\n")
def __str__(self):
return f"BankAccount({self.owner}, ₹{self.balance})"
# Let's use it
acc = BankAccount("Kiran", 5000)
acc.deposit(2000) # Deposited ₹2000. New balance: ₹7000
acc.withdraw(1500) # Withdrew ₹1500. New balance: ₹5500
acc.withdraw(10000) # Insufficient funds.
acc.get_statement()
print(acc) # BankAccount(Kiran, ₹5500)
4. Inheritance — Building on What Exists
You inherit certain things from your parents — maybe your mother's height, your father's sense of humour — but you also have your own personality that's entirely yours. Inheritance in OOP works the same way. A child class inherits all the attributes and methods of its parent class, and can also add new ones or change how existing ones work.
This prevents you from duplicating code. If you have a Dog class and a Cat class that both need a name and eat() method, don't write those twice — put them in an Animal class and inherit from it.
super().__init__() — Calling the Parent
When a child class has its own __init__, you need to call the parent's __init__ to set up the inherited attributes. You do this with super().__init__().
class Animal:
"""Base class — all animals have these."""
def __init__(self, name, species):
self.name = name
self.species = species
self.is_alive = True
def eat(self):
print(f"{self.name} is eating.")
def sleep(self):
print(f"{self.name} is sleeping.")
def __str__(self):
return f"{self.species} named {self.name}"
class Dog(Animal): # Dog inherits from Animal
"""Dogs are Animals, but they also bark and have a breed."""
def __init__(self, name, breed):
super().__init__(name, species="Dog") # set up Animal part
self.breed = breed
def bark(self):
print(f"{self.name} says: Woof!")
def fetch(self, item):
print(f"{self.name} fetches the {item}!")
# Override the Animal's __str__ with a better one
def __str__(self):
return f"Dog: {self.name} ({self.breed})"
class GuideDog(Dog): # GuideDog inherits from Dog
"""A guide dog has all Dog capabilities + special training."""
def __init__(self, name, breed, owner):
super().__init__(name, breed) # set up Dog part
self.owner = owner
self.is_certified = True
def guide(self):
print(f"{self.name} is guiding {self.owner} safely.")
def __str__(self):
return f"Guide Dog: {self.name} — serves {self.owner}"
# Let's use the whole chain
buddy = Dog("Buddy", "Labrador")
buddy.eat() # From Animal — Buddy is eating.
buddy.bark() # From Dog — Buddy says: Woof!
print(buddy) # Dog: Buddy (Labrador)
rex = GuideDog("Rex", "German Shepherd", "Mr. Thomas")
rex.eat() # From Animal
rex.bark() # From Dog
rex.guide() # From GuideDog — Rex is guiding Mr. Thomas safely.
print(rex) # Guide Dog: Rex — serves Mr. Thomas
GuideDog inherits from Dog, which inherits from Animal. So a GuideDog object has everything from all three classes. When you call a method, Python looks in GuideDog first, then Dog, then Animal. This is called the Method Resolution Order (MRO).
5. Practical Project: Library Book System
Let's design a small library management system. This is a realistic OOP design — the kind you'd actually build in a software engineering course or a real project. Three classes working together: Book, Member, and Library.
class Book:
"""Represents a single book in the library."""
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = True
def __str__(self):
status = "Available" if self.is_available else "Borrowed"
return f'"{self.title}" by {self.author} [{status}]'
class Member:
"""A registered library member."""
def __init__(self, name, member_id):
self.name = name
self.member_id = member_id
self.borrowed_books = []
def borrow(self, book):
self.borrowed_books.append(book)
def return_book(self, book):
if book in self.borrowed_books:
self.borrowed_books.remove(book)
def __str__(self):
count = len(self.borrowed_books)
return f"Member {self.name} (ID: {self.member_id}) — {count} book(s) borrowed"
class Library:
"""Manages books and members."""
def __init__(self, name):
self.name = name
self.books = []
self.members = []
def add_book(self, book):
self.books.append(book)
print(f"Added: {book.title}")
def register_member(self, member):
self.members.append(member)
print(f"Registered: {member.name}")
def borrow_book(self, member, isbn):
for book in self.books:
if book.isbn == isbn:
if book.is_available:
book.is_available = False
member.borrow(book)
print(f'{member.name} borrowed "{book.title}".')
return
else:
print(f'"{book.title}" is already borrowed.')
return
print(f"No book with ISBN {isbn} found.")
def return_book(self, member, isbn):
for book in member.borrowed_books:
if book.isbn == isbn:
book.is_available = True
member.return_book(book)
print(f'{member.name} returned "{book.title}".')
return
print("This book is not in member's borrowed list.")
def list_available(self):
print(f"\n--- Available books at {self.name} ---")
available = [b for b in self.books if b.is_available]
if available:
for book in available:
print(f" {book}")
else:
print(" No books currently available.")
print()
# --- Run the system ---
lib = Library("Quadratech Knowledge Hub")
# Add books
b1 = Book("Python Crash Course", "Eric Matthes", "978-1-7185-0075-3")
b2 = Book("Clean Code", "Robert C. Martin", "978-0-1323-5088-4")
b3 = Book("The Pragmatic Programmer", "David Thomas", "978-0-1359-5028-0")
lib.add_book(b1)
lib.add_book(b2)
lib.add_book(b3)
# Register members
m1 = Member("Priya", "M001")
m2 = Member("Arjun", "M002")
lib.register_member(m1)
lib.register_member(m2)
# Borrow and return
lib.list_available()
lib.borrow_book(m1, "978-1-7185-0075-3")
lib.borrow_book(m2, "978-1-7185-0075-3") # Already borrowed
lib.list_available()
lib.return_book(m1, "978-1-7185-0075-3")
lib.list_available()
Add a borrow_date attribute to the borrowing system. Then write a method calculate_fine(return_date) on the Member class that calculates a fine of Rs. 2 per day for books returned late (after 14 days). Use Python's datetime module to work with dates.
