Classes and Objects
What is OOP?
So far you've written procedural code — a list of instructions that run one by one, with functions to organize them.
OOP is a completely different way of thinking about code. Instead of writing instructions, you model real world things as objects in your code.
Real World Analogy — The Blueprint
Think of a class as a blueprint and an object as the actual thing built from that blueprint.
Blueprint of a Car (Class)
- Has: color, brand, speed, fuel
- Can: start, stop, accelerate, brake
Actual Car 1 (Object): Red Toyota, speed=0
Actual Car 2 (Object): Blue Honda, speed=60
Actual Car 3 (Object): Black BMW, speed=120
One blueprint — many cars. Each car has the same properties and abilities but different values.
Same in Python:
Student Class (Blueprint)
- Has: name, age, marks
- Can: introduce(), get_grade(), study()
student1 = Student("Rahul", 20, 85)
student2 = Student("Priya", 21, 92)
student3 = Student("Gagan", 22, 78)
Your First Class
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 introduce(self): print(f"I am {self.name}, a {self.breed}, {self.age} years old")
Let's break this down piece by piece.
class keyword
class Dog:
class tells Python you're defining a class. Dog is the class name. Convention — class names start with Capital Letter.
__init__ — The Constructor
def __init__(self, name, breed, age): self.name = name self.breed = breed self.age = age
__init__ is a special method that runs automatically when you create an object. It's called the constructor — it initializes (sets up) the object.
self — refers to the object being created. When you create dog1, self means dog1. When you create dog2, self means dog2.
self.name = name — stores the name parameter as an attribute of the object. Attributes are variables that belong to the object.
Creating Objects
dog1 = Dog("Bruno", "Labrador", 3) dog2 = Dog("Max", "German Shepherd", 5) dog3 = Dog("Charlie", "Poodle", 2)
This is called instantiation — creating an instance (object) of a class.
Each object is completely independent with its own data.
Accessing Attributes and Calling Methods
dog1 = Dog("Bruno", "Labrador", 3)
# Accessing attributes print(dog1.name) # Bruno print(dog1.breed) # Labrador print(dog1.age) # 3
# Calling methods dog1.bark() # Bruno says: Woof! dog1.introduce() # I am Bruno, a Labrador, 3 years old
Full Example — All Three Dogs
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 introduce(self): print(f"I am {self.name}, a {self.breed}, {self.age} years old")
def birthday(self): self.age += 1 print(f"Happy Birthday {self.name}! Now {self.age} years old")
dog1 = Dog("Bruno", "Labrador", 3) dog2 = Dog("Max", "German Shepherd", 5) dog3 = Dog("Charlie", "Poodle", 2)
dog1.introduce() dog2.introduce() dog3.introduce()
print()
dog1.bark() dog2.bark()
print()
dog1.birthday() print(f"Bruno's age is now: {dog1.age}") # 4 print(f"Max's age is still: {dog2.age}") # 5 — unchanged
Output:
I am Bruno, a Labrador, 3 years old
I am Max, a German Shepherd, 5 years old
I am Charlie, a Poodle, 2 years old
Bruno says: Woof!
Max says: Woof!
Happy Birthday Bruno! Now 4 years old
Bruno's age is now: 4
Max's age is still: 5
Methods vs Functions
A function is defined outside a class with def:
def greet(name): print(f"Hello {name}")
A method is a function defined inside a class. It always has self as first parameter:
class Person: def greet(self): # method — belongs to class print(f"Hello {self.name}")
They work the same way but methods are tied to a specific object.
self Explained Simply
self is just a reference to the current object. When you call:
dog1.bark()
Python internally does:
Dog.bark(dog1) # passes dog1 as self
So inside bark(), when you write self.name — it means dog1.name. Simple.
You could technically name it anything but self is the universal convention. Always use self.
Real World Class — Bank Account
class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance self.transactions = []
def deposit(self, amount): if amount <= 0: print("Deposit amount must be positive") return self.balance += amount self.transactions.append(f"Deposited: Rs.{amount}") print(f"Rs.{amount} deposited. New balance: Rs.{self.balance}")
def withdraw(self, amount): if amount <= 0: print("Withdrawal amount must be positive") return if amount > self.balance: print(f"Insufficient balance. Available: Rs.{self.balance}") return self.balance -= amount self.transactions.append(f"Withdrawn: Rs.{amount}") print(f"Rs.{amount} withdrawn. New balance: Rs.{self.balance}")
def get_balance(self): print(f"\nAccount: {self.owner}") print(f"Balance: Rs.{self.balance}")
def get_statement(self): print(f"\n=== Statement for {self.owner} ===") if not self.transactions: print("No transactions yet") else: for t in self.transactions: print(f" {t}") print(f"Current Balance: Rs.{self.balance}")
# Create two accounts acc1 = BankAccount("Gagan", 1000) acc2 = BankAccount("Rahul") # starts with 0
acc1.deposit(5000) acc1.withdraw(2000) acc1.withdraw(10000) # should fail — insufficient balance acc1.get_statement()
print()
acc2.deposit(3000) acc2.get_balance()
Output:
Rs.5000 deposited. New balance: Rs.6000
Rs.2000 withdrawn. New balance: Rs.4000
Insufficient balance. Available: Rs.4000
=== Statement for Gagan ===
Deposited: Rs.5000
Withdrawn: Rs.2000
Current Balance: Rs.4000
Rs.3000 deposited. New balance: Rs.3000
Account: Rahul
Balance: Rs.3000
This is real OOP — data (balance, transactions) and behavior (deposit, withdraw) bundled together in one object.
Class Attributes vs Instance Attributes
Instance attributes — unique to each object (what we've been using):
self.name = name # each dog has its own name
Class attributes — shared by ALL objects of the class:
class Dog: species = "Canis familiaris" # class attribute — same for all dogs
def __init__(self, name): self.name = name # instance attribute — different per dog
dog1 = Dog("Bruno") dog2 = Dog("Max")
print(dog1.species) # Canis familiaris print(dog2.species) # Canis familiaris print(dog1.name) # Bruno print(dog2.name) # Max
print(Dog.species) # can also access via class name
__str__ — String Representation
When you print an object directly you get something ugly:
dog1 = Dog("Bruno", "Labrador", 3) print(dog1) # <__main__.Dog object at 0x7f...> — ugly!
Define __str__ to control what prints:
class Dog: def __init__(self, name, breed, age): self.name = name self.breed = breed self.age = age
def __str__(self): return f"Dog({self.name}, {self.breed}, {self.age} years)"
dog1 = Dog("Bruno", "Labrador", 3) print(dog1) # Dog(Bruno, Labrador, 3 years)
__str__ is called automatically when you use print() or str() on your object. Always define it — makes debugging much easier.
Inheritance — One Class Extending Another
Inheritance lets you create a new class based on an existing one — it inherits all attributes and methods of the parent class.
# Parent class class Animal: def __init__(self, name, age): self.name = name self.age = age
def eat(self): print(f"{self.name} is eating")
def sleep(self): print(f"{self.name} is sleeping")
def __str__(self): return f"{self.name} (age {self.age})"
# Child class — inherits from Animal class Dog(Animal): def __init__(self, name, age, breed): super().__init__(name, age) # call parent's __init__ self.breed = breed # add extra attribute
def bark(self): print(f"{self.name} says: Woof!")
def fetch(self): print(f"{self.name} fetches the ball!")
class Cat(Animal): def __init__(self, name, age, indoor): super().__init__(name, age) self.indoor = indoor
def meow(self): print(f"{self.name} says: Meow!")
def purr(self): print(f"{self.name} is purring...")
dog = Dog("Bruno", 3, "Labrador") cat = Cat("Whiskers", 2, True)
# Dog has Animal methods AND its own methods dog.eat() # from Animal — Bruno is eating dog.sleep() # from Animal — Bruno is sleeping dog.bark() # from Dog — Bruno says: Woof! dog.fetch() # from Dog — Bruno fetches the ball!
print()
cat.eat() # from Animal cat.meow() # from Cat cat.purr() # from Cat
print(dog) # Bruno (age 3) print(cat) # Whiskers (age 2)
Output:
Bruno is eating
Bruno is sleeping
Bruno says: Woof!
Bruno fetches the ball!
Whiskers is eating
Whiskers says: Meow!
Whiskers is purring...
Bruno (age 3)
Whiskers (age 2)
super() Explained
class Dog(Animal): def __init__(self, name, age, breed): super().__init__(name, age) # runs Animal's __init__ self.breed = breed
super() calls the parent class. Without super().__init__(), the name and age attributes from Animal would never get set. Always call super().__init__() in child class constructor.
Overriding Methods
Child class can override (replace) parent's method:
class Animal: def speak(self): print("Some sound")
class Dog(Animal): def speak(self): # overrides Animal's speak print(f"{self.name} says: Woof!")
class Cat(Animal): def speak(self): # overrides Animal's speak print(f"{self.name} says: Meow!")
class Duck(Animal): def speak(self): # overrides Animal's speak print(f"{self.name} says: Quack!")
animals = [Dog("Bruno", 3, "Lab"), Cat("Whiskers", 2, True), Duck("Donald", 4)]
# Same method call — different behavior based on object type # This is called Polymorphism for animal in animals: animal.speak()
Output:
Bruno says: Woof!
Whiskers says: Meow!
Donald says: Quack!
This is Polymorphism — same method name, different behavior depending on the object. One of the core OOP concepts.
Encapsulation — Protecting Data
Encapsulation means hiding internal data and only exposing what's necessary.
In Python, use _ (single underscore) to indicate "private" — don't access directly:
class BankAccount: def __init__(self, owner, balance): self.owner = owner self._balance = balance # _ means "private, don't touch directly"
def get_balance(self): # controlled access return self._balance
def deposit(self, amount): if amount > 0: self._balance += amount # controlled modification
acc = BankAccount("Gagan", 1000)
# Bad practice — accessing private attribute directly print(acc._balance) # works but you shouldn't do this
# Good practice — use the method print(acc.get_balance()) # 1000 acc.deposit(500) print(acc.get_balance()) # 1500
Python doesn't strictly enforce private attributes like some other languages but the _ convention signals to other developers "don't access this directly."
Real World Example — Student Management System with OOP
class Student: student_count = 0 # class attribute — tracks total students
def __init__(self, name, age, email): self.name = name self.age = age self.email = email self.marks = [] self.student_id = Student.student_count + 1 Student.student_count += 1
def add_marks(self, subject, mark): self.marks.append({"subject": subject, "mark": mark})
def get_average(self): if not self.marks: return 0 total = sum(m["mark"] for m in self.marks) return total / len(self.marks)
def get_grade(self): avg = self.get_average() if avg >= 90: return "A" elif avg >= 80: return "B" elif avg >= 70: return "C" elif avg >= 60: return "D" else: return "F"
def print_report(self): print(f"\n{'='*35}") print(f" STUDENT REPORT CARD") print(f"{'='*35}") print(f" ID : {self.student_id}") print(f" Name : {self.name}") print(f" Age : {self.age}") print(f" Email : {self.email}") print(f"{'='*35}") if self.marks: for m in self.marks: print(f" {m['subject']:<15}: {m['mark']}") print(f"{'='*35}") print(f" Average : {self.get_average():.2f}") print(f" Grade : {self.get_grade()}") else: print(" No marks added yet") print(f"{'='*35}")
def __str__(self): return f"Student({self.student_id}: {self.name}, Grade: {self.get_grade()})"
class Classroom: def __init__(self, class_name): self.class_name = class_name self.students = []
def add_student(self, student): self.students.append(student) print(f"{student.name} added to {self.class_name}")
def find_student(self, name): for student in self.students: if student.name.lower() == name.lower(): return student return None
def get_top_student(self): if not self.students: return None return max(self.students, key=lambda s: s.get_average())
def print_summary(self): print(f"\n=== {self.class_name} Summary ===") print(f"Total students: {len(self.students)}") for student in self.students: print(f" {student}")
# Using the system classroom = Classroom("Python Batch 2025")
s1 = Student("Rahul", 20, "rahul@email.com") s2 = Student("Priya", 21, "priya@email.com") s3 = Student("Gagan", 22, "gagan@email.com")
classroom.add_student(s1) classroom.add_student(s2) classroom.add_student(s3)
s1.add_marks("Python", 85) s1.add_marks("Math", 78) s1.add_marks("English", 82)
s2.add_marks("Python", 92) s2.add_marks("Math", 95) s2.add_marks("English", 88)
s3.add_marks("Python", 78) s3.add_marks("Math", 72) s3.add_marks("English", 80)
s1.print_report() s2.print_report()
classroom.print_summary()
top = classroom.get_top_student() print(f"\nTop student: {top.name} with average {top.get_average():.2f}") print(f"Total students enrolled: {Student.student_count}")
Output:
Rahul added to Python Batch 2025
Priya added to Python Batch 2025
Gagan added to Python Batch 2025
===================================
STUDENT REPORT CARD
===================================
ID : 1
Name : Rahul
Age : 20
Email : rahul@email.com
===================================
Python : 85
Math : 78
English : 82
===================================
Average : 81.67
Grade : B
===================================
...
=== Python Batch 2025 Summary ===
Total students: 3
Student(1: Rahul, Grade: B)
Student(2: Priya, Grade: A)
Student(3: Gagan, Grade: C)
Top student: Priya with average 91.67
Total students enrolled: 3
Four Pillars of OOP — Summary
|
Pillar |
Meaning |
Example |
|
Encapsulation |
Bundle
data and methods together, hide internals |
_balance in BankAccount |
|
Inheritance |
Child class extends parent
class |
Dog extends Animal |
|
Polymorphism |
Same
method, different behavior |
speak() in Dog, Cat, Duck |
|
Abstraction |
Hide complexity, show only
what's needed |
deposit() hides balance logic |
Exercise 🏋️
Build a Library Management System using OOP:
Create these classes:
Book class:
- Attributes: title, author, isbn, is_available (default True)
- Methods:
__str__,borrow(),return_book()
Member class:
- Attributes: name, member_id, borrowed_books (list)
- Methods:
borrow_book(book),return_book(book),print_profile()
Library class:
- Attributes: name, books (list), members (list)
- Methods:
add_book(book),register_member(member),search_book(title),print_catalog()
Requirements:
- Member cannot borrow a book that's already borrowed
- Member cannot borrow more than 3 books at once
- When book is returned, it becomes available again
print_catalog()shows all books with availability status
This is a real system — using everything from all 9 stages together!
No comments:
Post a Comment