The Need for OOP Beyond Procedural Programming
Understand what OOP is and why it’s needed.
We'll cover the following...
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:
# Data stored in separate liststitles = ["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.
# Function to display booksdef display_books():for i in range(len(titles)):status = statuses[i]print(titles[i] + " - " + status)# Display booksprint("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 statusestitles.append("Dune") # Forgot to update `statuses`
What happens if we display the library now?
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"
.
# Function to borrow a bookdef 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 missingelse: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:
# Trying to borrow the new booktry: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?
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:
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 OrwellTitle: 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 thanbook.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:
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 detailsfor i, book in enumerate(library):# Check if the title of the current book matches the requested titleif 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 immutablelibrary[i] = (book[0], "Checked Out", book[2], book[3])print(title, "borrowed successfully.") # Print confirmation messageelse:# If the book is already checked out (not available), print that it's already borrowedprint(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 messageprint(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.
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.
# Step 1: Creating the Book class with attributesclass Book:def __init__(self, title, status="Available"):self.title = titleself.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 parameterself
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 afterself
is used to access attributes or methods of the object. For example,self.title = title
assigns the value oftitle
to the attributetitle
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.
class Book:def __init__(self, title, status="Available"):self.title = titleself.status = statusdef 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.”
class Book:def __init__(self, title, status="Available"):self.title = titleself.status = statusdef 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.
class Book:def __init__(self, title, status="Available"):self.title = titleself.status = statusdef 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 objectsbook1 = 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.
class Book:def __init__(self, title, status="Available"):self.title = titleself.status = statusdef 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 booksbook1.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.
class Book:def __init__(self, title, status="Available"):self.title = titleself.status = statusdef 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 bookbook3.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!