Loading

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.

The Core Idea

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.

Python — Student class with attributes and a method
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 vs Methods

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.

Python — BankAccount class with practical methods
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__().

Python — Animal → Dog → GuideDog inheritance chain
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
Inheritance Chain

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.

Python — Library Book System (full OOP project)
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()
Try It Yourself

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.