...

/

The Need for OOP Beyond Procedural Programming

The Need for OOP Beyond Procedural Programming

Understand what OOP is and why it’s needed.

As you’ve been programming for a while, you’re likely familiar with writing functions to perform specific tasks. These functions help break down a program into smaller, manageable parts.

This approach is the essence of procedural programming, a paradigm in which programs are structured as a sequence of procedures or functions. Each function handles a specific part of the task, promoting reusability and simplicity.

However, while procedural programming works well for straightforward applications, it can become challenging when applied to complex, real-world scenarios. As the system grows, managing interdependencies between data and functions becomes increasingly difficult, leading to errors and scalability issues.

Imagine managing a library with a growing collection of books. You need to keep track of book titles, their availability, and who can borrow them. While managing this data seems simple, chaos emerges as the library grows.

This is where object-oriented programming (OOP) comes into play, helping you turn messy, scattered code into clean, well-organized solutions. However, before starting our OOP journey, let’s look at how we might handle this library management with a simpler approach: procedural programming.

Procedural programming

In procedural programming, data and functions are kept separate. While this approach works for small projects, it leads to mismatched data, logical errors, and poor scalability as complexity increases.

Let’s see how we’d manage a library using this approach.

Representing the library data

We store information about the library (book titles and their statuses) in separate lists:

Press + to interact
# Data stored in separate lists
titles = ["1984", "The Hobbit"]
statuses = ["Available", "Checked Out"]

Each list represents one book’s attribute. The above lines of code represent that the book titled “1884” is available for borrowing, while “The Hobbit” has already been checked out by someone. While this works for small libraries, things get tricky as more attributes are added (e.g., authors).

Displaying the library

To show the current state of the library, we write a function that iterates over the titles list and fetches the corresponding status from the statuses list.

Press + to interact
# Function to display books
def display_books():
for i in range(len(titles)):
status = statuses[i]
print(titles[i] + " - " + status)
# Display books
print("Initial Library State:")
display_books()

This works fine initially, but it already relies on the lengths of both lists being the same. Any mismatch will cause issues.

Adding a new book

Let’s add a new book to the library.

# Adding a new book without updating statuses
titles.append("Dune") # Forgot to update `statuses`
Adding a new book to the library

What happens if we display the library now?

Press + to interact
print("\nAfter Adding 'Dune':")
display_books()

The book "Dune" appears, but its status is missing because we didn’t update the statuses list. This mismatch is a common issue in procedural programming.

Borrowing a book

Now, let’s write a function to borrow a book by changing its status to "Checked Out".

Press + to interact
# Function to borrow a book
def borrow_book(book_title):
if book_title in titles:
index = titles.index(book_title)
statuses[index] = "Checked Out" # This can cause an error if the index is missing
else:
print("Book " + book_title + "not found in the library.")

Lines 4 and 5 find the index of the specified book title in the titles list and then update the corresponding element in the statuses list to Checked Out, effectively marking the book as borrowed. If we try to borrow "Dune", we encounter an issue:

Press + to interact
# Trying to borrow the new book
try:
borrow_book("Dune")
except IndexError as e:
print("Error:", e)

The error occurs because the statuses list doesn’t have an entry for "Dune". This highlights how separate lists can become desynchronized, leading to runtime errors.

Quick question

Before moving forward, take a moment to answer the following question to summarize the list of problems with procedural programming to assess your understanding so far!

In a library system using a procedural programming approach—where book titles, statuses, and other attributes like authors are kept in separate lists—what challenges might arise from issues like data mismatch, error-prone logic, and scalability?

How do these challenges impact the system’s data integrity and long-term maintainability?

Our AI is here to assist!

The need for structured data

The core issue here is clear:

Data belonging to the same entity is scattered across multiple structures.

Procedural programming, in its purest form, doesn’t inherently support grouping related data conveniently. We need a way to bundle related data together in a simpler, yet structured manner.

Tuples

Python offers a simple data structure called a tuple, allowing us to group related pieces of data:

book1 = ("1984", "Available", "George Orwell")
book2 = ("The Hobbit", "Checked Out", "J.R.R. Tolkien")

Now, each book is neatly represented as a single entity. Our library becomes:

library = [book1, book2]

We can iterate over this library easily:

Press + to interact
for book in library:
print("Title:", book[0], "| Status:", book[1], "| Author:", book[2])

And we’ll get the output as:

Title: 1984 | Status: Available | Author: George Orwell
Title: The Hobbit | Status: Checked Out | Author: J.R.R. Tolkien

Advantages of tuples

  • Related attributes are grouped, reducing mismatches.

  • Easy to manage individual books.

Limitations of tuples

  • You must remember the meaning of each tuple position (the title is [0], status is [1]).

  • Data access isn’t intuitive (book[0] is less clear than book.title).

  • No built-in methods; actions (like borrowing a book) still require external functions.

If we look at the last limitation, we’ll realize that borrowing a book still requires external logic:

Press + to interact
def borrow_book_tuple(library, title):
# Loop through the library (list of books), with `i` as the index and `book` as the tuple containing book details
for i, book in enumerate(library):
# Check if the title of the current book matches the requested title
if book[0] == title:
# If the book is found, check if it is available (book[1] represents the status)
if book[1] == "Available":
# If the book is available, update its status to "Checked Out"
# A new tuple is created, as tuples are immutable
library[i] = (book[0], "Checked Out", book[2], book[3])
print(title, "borrowed successfully.") # Print confirmation message
else:
# If the book is already checked out (not available), print that it's already borrowed
print(title, "already borrowed.")
return # Exit the function once the book has been found and processed
# If the book with the given title is not found in the library, print a message
print(title, "not found in library.")

Tuples are immutable in Python. This means that once a tuple is created, its elements cannot be changed or modified in place.

In the borrow_book_tuple() function, we try to modify the second element of a tuple (the status of the book) from "Available" to "Checked Out", but we can’t directly modify an individual element of a tuple. This is a limitation of tuples.

While better organized than separate lists, tuples remain limited and somewhat cumbersome. Tuples offer an improved data structure compared to procedural programming’s scattered lists. However, the lack of readability and intuitive interaction with tuples still creates friction, especially as your system scales.

What if data and actions could be grouped naturally, like real-world objects?

The solution: Object-oriented programming

With OOP, we can bundle all related data (like title and status) and actions (like borrow or display) into a single object. This prevents data mismatches and organizes the code logically. Before solving the issue, let’s understand OOP itself!

What is OOP?

At its core, object-oriented programming (OOP) is a way of designing programs around objects.

An object is a blueprinted “thing” in your code that combines:

  • Attributes (data): What the object has (e.g., a book’s title and author).

  • Methods: What the object can do (e.g., borrow or display a book).

In procedural programming, you learned to write functions—self-contained blocks of code that take inputs, perform tasks, and return outputs. These functions work independently and are not inherently tied to any particular data structure.

One way procedural programming temporarily groups related data is through tuples, which allow storing multiple related values in a single, immutable collection. However, tuples don’t offer methods to directly manipulate their data, leading us toward object-oriented programming (OOP), where we organize code around objects.

When you define a function inside a class, it becomes a method. The key difference is that methods are designed to act on objects, and always take a special parameter (usually named self) that refers to the instance on which the method is being called.

Coming back to our main topic, we use a class to create objects, which acts as a blueprint.

Press + to interact
Object-oriented programming representation of library
Object-oriented programming representation of library

Now, let’s try to solve this problem with object-oriented programming!

To break down the object-oriented programming (OOP) example step by step, here’s how we can proceed:

Step 1: Creating a class

In OOP, we create a class to define the blueprint for objects. The Book class will represent the library’s books. We begin by defining the class and its attributes (title, status) in the __init__ method.

Press + to interact
# Step 1: Creating the Book class with attributes
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status

Here:

  • The title is passed when creating a book.

  • The status is set to “Available” by default if not specified.

Let’s understand the syntax as well.

  • __init__ method: This is a special method in Python known as the initializer or constructor. It is automatically called when a new instance of the class is created, and its main role is to set up the new object's initial state by assigning values to its attributes.

  • self parameter: The parameter self represents the instance of the class that is being created or manipulated. It allows the method to access or modify attributes and call other methods on that specific object.

  • Dot notation (self.title): The dot after self is used to access attributes or methods of the object. For example, self.title = title assigns the value of title to the attribute title of the current object.

Step 2: Adding a method to display book information

Now, we add a method inside the class to display the book’s title and status.

Press + to interact
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status
def display(self):
print(self.title + " - " + self.status)

Lines 6–7: This method is associated with each book object and prints its title and current status. The display method in the Book class prints the book’s title and its status in a readable format. Inside the method, self.title and self.status refer to the title and status attributes of the specific Book instance (in this case, book1). When we click the “Run” button, it outputs the title and status of book1 (it is set on the backend for now), which is “First book—Available” (since no status was provided, it defaults to “Available”).

Step 3: Adding a method to borrow a book

Next, we implement a method that allows users to borrow a book, which changes the status from “Available” to “Checked Out.”

Press + to interact
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status
def display(self):
print(self.title + " - " + self.status)
def borrow(self):
if self.status == "Available":
self.status = "Checked Out"
print(self.title + " has been borrowed.")
else:
print(self.title + " is already checked out.")

Lines 9–14: The method borrow(self) checks if the book is available. If so, it updates the status. If the book is already borrowed, it notifies the user. For now, we have set a book, “First book” on the backend and called the borrow method on it. When we click the “Run” button, we’ll see the output "First book has been borrowed.".

Step 4: Creating book objects

With the class and methods defined, we create instances of the Book class to represent individual books in the library.

Press + to interact
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status
def display(self):
print(self.title + " - " + self.status)
def borrow(self):
if self.status == "Available":
self.status = "Checked Out"
print(self.title + " has been borrowed.")
else:
print(self.title + " is already checked out.")
# Creating book objects
book1 = Book("1984")
book2 = Book("The Hobbit", "Checked Out")
book3 = Book("Dune") # Adding a new book

Lines 17–19: Here, book1, book2, and book3 are objects (instances) of the Book class, each representing a different book.

Step 5: Displaying the books

Now, we can use the display method to show the state of each book.

Press + to interact
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status
def display(self):
print(self.title + " - " + self.status)
def borrow(self):
if self.status == "Available":
self.status = "Checked Out"
print(self.title + " has been borrowed.")
else:
print(self.title + " is already checked out.")
book1 = Book("1984")
book2 = Book("The Hobbit", "Checked Out")
book3 = Book("Dune")
# Displaying the books
book1.display()
book2.display()
book3.display()

Lines 21–23: This will output the title and status of each book.

Step 6: Borrowing a book

Let’s try borrowing a book and see how the status updates.

Press + to interact
class Book:
def __init__(self, title, status="Available"):
self.title = title
self.status = status
def display(self):
print(self.title + " - " + self.status)
def borrow(self):
if self.status == "Available":
self.status = "Checked Out"
print(self.title + " has been borrowed.")
else:
print(self.title + " is already checked out.")
book1 = Book("1984")
book2 = Book("The Hobbit", "Checked Out")
book3 = Book("Dune")
# Borrowing a book
book3.borrow()
book3.display()

Lines 21–22: After borrowing, the status of "Dune" changes to "Checked Out". In OOP, this process is straightforward and error-free because the book3 object handles its own data.

Unlike procedural programming, where we had to manually update multiple lists (risking errors like mismatched data), OOP ensures that each book object maintains its own status. When we call the borrow() method on "Dune", it updates the status internally, preventing issues like the IndexError we faced earlier.

With OOP, there’s no need to worry about synchronizing data or missing updates—everything is contained within the object, making the borrowing process clean, reliable, and free from logical errors.

Procedural programming is like managing your library with sticky notes—prone to errors, disorganized, and hard to maintain. OOP gives you a proper system where each book knows its details and can handle its own behavior, making your code clean, organized, and scalable.

Ready to dive deeper into OOP?

Let’s explore how to transform messy procedural code into clean, scalable solutions!