Classes and Objects — Building Modular, Reusable Code

Object-Oriented Programming (OOP) is a programming paradigm that models software around **objects** — real-world entities that have **attributes** (data) and **methods** (behaviors).

Chapter 5: Object-Oriented Programming (OOP)

Sub-chapter: Classes and Objects — Building Modular, Reusable Code

Object-Oriented Programming (OOP) is a programming paradigm that models software around objects — real-world entities that have attributes (data) and methods (behaviors).
In Python, everything is an object — classes provide the blueprints, and objects are the instances built from them.


🧩 Why OOP?

ApproachDescriptionExample Use
ProceduralCode organized as functions and data structuresSmall scripts, utilities
Object-OrientedCode organized around objects and classesGames, APIs, enterprise systems

OOP improves modularity, reusability, and scalability — essential for modern software development.


🧱 Defining a Class

A class is a template for creating objects.
It defines attributes (variables) and methods (functions) that describe object behavior.

class Person:
    def __init__(self, name, age):
        self.name = name      # Instance attribute
        self.age = age

    def greet(self):
        return f"Hello, my name is {self.name} and I am {self.age} years old."

Creating and Using Objects

alice = Person("Alice", 30)
bob = Person("Bob", 25)

print(alice.greet())
print(bob.name)

Each object (alice, bob) has its own state (data) and can perform actions (methods).


🧠 The self Keyword

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(f"{self.name} says woof!")

🏗️ Class vs Instance Attributes

class Circle:
    pi = 3.14159  # Class attribute (shared)

    def __init__(self, radius):
        self.radius = radius  # Instance attribute (unique)

    def area(self):
        return Circle.pi * (self.radius ** 2)

🧰 Constructors and __init__

The __init__ method is called automatically when an object is created — used to initialize data.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f"'{self.title}' by {self.author}"

💬 String Representations: __str__ and __repr__

MethodPurposeExample Output
__str__()User-friendly string for printing"Book('1984', 'Orwell')"
__repr__()Developer-focused representation"Book(title='1984', author='Orwell')"
class Example:
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return f"Example with value {self.value}"

    def __repr__(self):
        return f"Example(value={self.value!r})"

🧩 Class Methods and Static Methods

@classmethod

Operates on the class itself, not individual instances.

class Car:
    cars_built = 0

    def __init__(self, model):
        self.model = model
        Car.cars_built += 1

    @classmethod
    def total_built(cls):
        return f"Total cars built: {cls.cars_built}"

@staticmethod

Does not depend on class or instance state.

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(10, 5))

🧱 Encapsulation and Data Protection

Python uses naming conventions for attribute visibility:

ConventionExampleMeaning
Publicself.nameAccessible everywhere
Protectedself._ageInternal use (by convention)
Privateself.__salaryName mangling prevents external access
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary  # Private attribute

    def show_info(self):
        return f"{self.name} earns ${self.__salary}"

🧩 Real-World Example — Library System

Let’s model a simple Library Management System using OOP principles.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.is_borrowed = False

    def borrow(self):
        if not self.is_borrowed:
            self.is_borrowed = True
            return f"You have borrowed '{self.title}'."
        return f"'{self.title}' is already borrowed."

    def return_book(self):
        self.is_borrowed = False
        return f"'{self.title}' has been returned."


class Member:
    def __init__(self, name):
        self.name = name
        self.borrowed_books = []

    def borrow_book(self, book):
        result = book.borrow()
        if "borrowed" in result:
            self.borrowed_books.append(book.title)
        return f"{self.name}: {result}"

    def show_books(self):
        return f"{self.name}'s borrowed books: {self.borrowed_books}"


class Library:
    def __init__(self, name):
        self.name = name
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def list_books(self):
        return [f"{b.title} by {b.author}" for b in self.books]

Example Usage

lib = Library("Central Library")
book1 = Book("1984", "George Orwell")
book2 = Book("Dune", "Frank Herbert")
lib.add_book(book1)
lib.add_book(book2)

member = Member("Alice")
print(member.borrow_book(book1))
print(member.show_books())
print(book1.return_book())

📊 UML-Style Class Diagram (Text Representation)

+----------------+       +----------------+       +----------------+
|    Library     |<>---->|     Book       |       |     Member     |
+----------------+       +----------------+       +----------------+
| - name         |       | - title        |       | - name          |
| - books        |       | - author       |       | - borrowed_books|
+----------------+       | - is_borrowed  |       +----------------+
| + add_book()   |       +----------------+       | + borrow_book() |
| + list_books() |       | + borrow()     |       | + show_books()  |
+----------------+       | + return_book()|       +----------------+

🧠 Library aggregates Book objects and interacts with Member instances to perform operations.


⚙️ Best Practices for Classes and Objects

✅ Keep each class focused on a single responsibility.
✅ Use meaningful names for attributes and methods.
✅ Use private attributes for sensitive data.
✅ Prefer composition over deep inheritance.
✅ Implement __str__ and __repr__ for better debugging.
✅ Keep classes small and modular — large classes are hard to maintain.


🧾 Summary


By mastering classes and objects, you gain the ability to design structured, modular, and scalable applications — the foundation for advanced OOP topics such as inheritance, polymorphism, and composition.