blog / Understanding OOP in the Real World: Building a To-Do App in Python

Understanding OOP in the Real World: Building a To-Do App in Python

oopspythoncscoding
November 10, 2025

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:

  1. Class – Think of it as a blueprint that defines what an object should look like and what it can do.
  2. Object – A real-world instance created from a class; it’s the actual thing you work with.
  3. Attributes – These are the variables inside a class that hold data about the object.
  4. 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.