Understanding OOP in the Real World: Building a To-Do App in Python
November 10, 2025read it on
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.
