We all learn about Object-Oriented Programming (OOP), classes, objects, and those four famous pillars: encapsulation, inheritance, polymorphism, and abstraction. Honestly, I never understood why we even learn this, but it turns out it does have some real-world use cases, and that’s exactly what we’re going to explore.
In this blog, we’re going to build one of the most popular applications, a To-Do app, using OOP concepts in Python.

Basics “jargon”
Before we dive in, there are a few jargons you should be familiar with:
- Class – Think of it as a blueprint that defines what an object should look like and what it can do.
- Object – A real-world instance created from a class; it’s the actual thing you work with.
- Attributes – These are the variables inside a class that hold data about the object.
- Methods – These are functions inside a class that define the object’s behavior or actions.
What Are These "Four Pillars" Everyone Talks About?
Almost every interviewer or viva examiner loves this question: “Explain the four pillars of OOP.”
They are encapsulation, inheritance, polymorphism, and abstraction.
But what exactly do these fancy terms mean in simple language?

Encapsulation: Keeping Things Tidy
Think of encapsulation as good housekeeping for your code. In our To-Do app, we’ll create classes that keep related data and methods bundled together while hiding the messy details from the outside world.
For example, our TodoItem class takes care of everything related to a single task:
class TodoItem:
def __init__(self, title):
self.__title = title
self.__completed = False
def mark_complete(self):
self.__completed = True
def get_status(self):
return "Completed" if self.__completed else "Pending"The beauty here? Other parts of the app don’t need to know how the task status is stored or changed.
They just call mark_complete() and get the job done.
The internal logic stays hidden and protected inside the class.
Note: __init__ is called automatically every time you create a new object from a class.
It’s known as the constructor, the method that initializes the object with its starting values.
Inheritance: Don’t Repeat Yourself
Tasks in our To-Do app can come in different types – work tasks, personal tasks, or recurring ones.
Rather than rewriting the same logic for each, we can create a base class and let others build on it.

class Task:
def __init__(self, title):
self.title = title
self.completed = False
def mark_complete(self):
self.completed = True
class WorkTask(Task):
def __init__(self, title, deadline):
super().__init__(title)
self.deadline = deadline
Here, WorkTask inherits all the features of Task and adds a deadline of its own.
If we ever want to add a new common method (like saving to a file), we can do it once in the parent class, and all child classes will get it automatically.
Note: self simply refers to the current object — the specific instance of the class that’s being worked on

Polymorphism: Same Action, Different Behavior
Polymorphism sounds fancy, but it’s simple: the same function name can have different behaviors depending on the object using it.
In our To-Do app, different types of tasks might have different ways of showing details.
class Task:
def show(self):
print(f"Task: {self.title}")
class WorkTask(Task):
def show(self):
print(f"Work Task: {self.title} (Deadline: {self.deadline})")
class PersonalTask(Task):
def show(self):
print(f"Personal Task: {self.title}")
Now, when we call show() on a list of mixed tasks, each one behaves appropriately without us writing multiple function names:
tasks = [WorkTask("Finish report", "2025-11-10"), PersonalTask("Buy groceries")]
for t in tasks:
t.show()
Abstraction: Simplifying the Complex
Users don’t need to know how the entire task system works — they just want to add, complete, or view tasks.
Abstraction helps us hide the complexity behind simple, clean interfaces.
from abc import ABC, abstractmethod
class TaskManager(ABC):
@abstractmethod
def add_task(self, title):
pass
@abstractmethod
def complete_task(self, title):
pass
class SimpleTaskManager(TaskManager):
def __init__(self):
self.tasks = []
def add_task(self, title):
self.tasks.append(Task(title))
def complete_task(self, title):
for t in self.tasks:
if t.title == title:
t.mark_complete()Here, TaskManager defines what actions a manager should support,
and SimpleTaskManager decides how those actions actually work internally.
The outside world just calls add_task() or complete_task() without caring about the internal logic.
Putting It All Together
Here’s what makes OOP so powerful, these four pillars work together to keep your project clean and scalable:
- Each task encapsulates its own data and logic
- Common functionality is inherited across different types of tasks
- Actions like
show()work polymorphically for all task types - Complex details are abstracted behind simple, clean interfaces
That’s how OOP turns a simple To-Do app into a maintainable, organized system you can easily expand over time.
