Day 15: Phase 2 Capstone — OOP-Based Task Manager

🐍 Day 15: Phase 2 Capstone — OOP-Based Task Manager


1. Learning Objectives

By the end of Day 15, you will be able to:

  • Design and implement a complete object‑oriented application from scratch
  • Combine inheritance, encapsulation, and polymorphism into a real system
  • Integrate file persistence (JSON) so data survives between runs
  • Apply exception handling to make the app robust
  • Structure a Python project with modules and packages
  • Write clean, maintainable OOP code that can be extended

2. Concept Explanation

2.1 The Big Picture — What We're Building

Today you'll build a Command‑Line Task Manager — a to‑do application that supports multiple task types, deadlines, priorities, and persistent storage. This project synthesises everything from Days 8–14.

Core features:

  • Add tasks of different types (simple task, deadline task, recurring task)
  • Mark tasks as completed
  • List tasks filtered by status or priority
  • Save tasks to a JSON file and load them on startup
  • Full error handling and input validation

2.2 OOP Design — From Concept to Code

Before writing a single line of code, think about your class hierarchy:

Task (base class)
├── SimpleTask
├── DeadlineTask
└── RecurringTask

Base class Task:

  • Attributes: title, description, priority, completed
  • Methods: mark_completed(), __str__(), to_dict(), from_dict()

Inheritance & Polymorphism: Each subclass overrides __str__() and adds its own behaviour. The TaskManager can work with any task type through the base class interface.

Composition: A TaskManager class has a list of tasks, not is a list. This keeps responsibilities separate.


2.3 Project Structure

We'll organise the code into modules:

task_manager/
├── main.py              # Entry point, menu loop
├── models/
│   ├── __init__.py
│   ├── task.py          # Base Task class
│   └── variants.py      # SimpleTask, DeadlineTask, RecurringTask
├── manager.py           # TaskManager class (CRUD + persistence)
└── utils.py             # Input validation helpers

2.4 Key Design Decisions

DecisionWhy
JSON for persistenceHuman‑readable, easy to debug, built‑in json module
to_dict() / from_dict() patternMakes serialisation clean and extensible
Class attribute for task typeEach subclass stores a string identifier ("simple", "deadline", "recurring") used for reconstruction
@staticmethod for input validationKeeps validation logic near the data it validates

3. Code Examples — Building Blocks

Step 1: Base Task Class

# models/task.py
from datetime import datetime

class Task:
    """Base class for all task types."""

    def __init__(self, title, description="", priority="medium"):
        self.title = title
        self.description = description
        self.priority = priority
        self.completed = False
        self.created_at = datetime.now().isoformat()

    def mark_completed(self):
        self.completed = True

    def __str__(self):
        status = "✓" if self.completed else "○"
        return f"[{status}] {self.title} ({self.priority})"

    def to_dict(self):
        """Convert task to a dictionary for JSON serialisation."""
        return {
            "type": self.__class__.task_type,
            "title": self.title,
            "description": self.description,
            "priority": self.priority,
            "completed": self.completed,
            "created_at": self.created_at,
        }

    @classmethod
    def from_dict(cls, data):
        """Create a task from a dictionary (to be overridden by subclasses)."""
        pass  # Implemented in each subclass

Step 2: Subclasses

# models/variants.py
from .task import Task

class SimpleTask(Task):
    task_type = "simple"

    def __str__(self):
        return f"[Simple] {super().__str__()}"

    @classmethod
    def from_dict(cls, data):
        task = cls(data["title"], data["description"], data["priority"])
        task.completed = data["completed"]
        task.created_at = data["created_at"]
        return task

class DeadlineTask(Task):
    task_type = "deadline"

    def __init__(self, title, description="", priority="medium", deadline=""):
        super().__init__(title, description, priority)
        self.deadline = deadline

    def __str__(self):
        return f"[Deadline: {self.deadline}] {super().__str__()}"

    def to_dict(self):
        data = super().to_dict()
        data["deadline"] = self.deadline
        return data

    @classmethod
    def from_dict(cls, data):
        task = cls(data["title"], data["description"], data["priority"], data["deadline"])
        task.completed = data["completed"]
        task.created_at = data["created_at"]
        return task

class RecurringTask(Task):
    task_type = "recurring"

    def __init__(self, title, description="", priority="medium", frequency="weekly"):
        super().__init__(title, description, priority)
        self.frequency = frequency

    def __str__(self):
        return f"[Recurring {self.frequency}] {super().__str__()}"

    def to_dict(self):
        data = super().to_dict()
        data["frequency"] = self.frequency
        return data

    @classmethod
    def from_dict(cls, data):
        task = cls(data["title"], data["description"], data["priority"], data["frequency"])
        task.completed = data["completed"]
        task.created_at = data["created_at"]
        return task

Step 3: Task Manager with Persistence

# manager.py
import json
from models.variants import SimpleTask, DeadlineTask, RecurringTask

TASK_CLASSES = {
    "simple": SimpleTask,
    "deadline": DeadlineTask,
    "recurring": RecurringTask,
}

class TaskManager:
    def __init__(self, filename="tasks.json"):
        self.filename = filename
        self.tasks = []
        self.load()

    def add_task(self, task):
        self.tasks.append(task)
        self.save()

    def list_tasks(self, show_completed=True):
        for i, task in enumerate(self.tasks, 1):
            if not show_completed and task.completed:
                continue
            print(f"{i}. {task}")

    def complete_task(self, index):
        if 0 <= index < len(self.tasks):
            self.tasks[index].mark_completed()
            self.save()
        else:
            print("Invalid task number.")

    def delete_task(self, index):
        if 0 <= index < len(self.tasks):
            removed = self.tasks.pop(index)
            print(f"Deleted: {removed.title}")
            self.save()
        else:
            print("Invalid task number.")

    def save(self):
        with open(self.filename, "w") as f:
            json.dump([t.to_dict() for t in self.tasks], f, indent=2)

    def load(self):
        try:
            with open(self.filename, "r") as f:
                data = json.load(f)
                self.tasks = [
                    TASK_CLASSES[item["type"]].from_dict(item)
                    for item in data
                ]
        except FileNotFoundError:
            self.tasks = []

4. Hands-On Exercises

Exercise 1: Build the Task Base Class

Create the Task class with the attributes and methods described above. Write a small test script to create a task, mark it completed, and print it.

Exercise 2: Implement a Subclass

Write the DeadlineTask subclass. Override __str__() and to_dict()/from_dict(). Test that an instance can be serialised to JSON and reconstructed correctly.

Exercise 3: JSON Persistence

Implement a simplified TaskManager that can:

  • Add a task
  • Save to a JSON file
  • Load from a JSON file
  • List all tasks Verify that tasks survive after restarting the program.

Exercise 4: Input Validation

Add exception handling in the manager: if the JSON file is corrupted or missing required keys, print a user‑friendly error instead of crashing.

Exercise 5: Polymorphic Display

Write a function display_tasks(tasks) that accepts a list of Task objects (any subclass) and prints them. Demonstrate with a list containing SimpleTask, DeadlineTask, and RecurringTask instances.


5. Applied Challenge Task 🏗️

Full Task Manager Application

Combine all the pieces into a working console application.

Core Requirements:

  1. Menu‑driven interface with options:

    === TASK MANAGER ===
    [1] Add Task
    [2] List All Tasks
    [3] List Pending Tasks
    [4] Complete Task
    [5] Delete Task
    [6] Save & Quit
    
  2. Add task flow:

    • Choose type: (S)imple, (D)eadline, (R)ecurring
    • Enter title, description, priority
    • For deadline: ask for date (YYYY‑MM‑DD)
    • For recurring: ask for frequency (daily, weekly, monthly)
  3. Input validations:

    • Priority must be low, medium, or high
    • Deadlines must be in valid date format
    • Task numbers must be within range
  4. Persistence: All tasks are saved to tasks.json when the user quits or after every modification

  5. Error handling: Graceful messages for file errors, invalid input, etc.

Stretch goals:

  • Edit an existing task
  • Search tasks by keyword
  • Sort tasks by priority or deadline
  • Add colour to the terminal output using colorama
  • Write unit tests for the Task and TaskManager classes (preview of Day 22)

6. Brief Review Summary

ConceptApplication in Project
InheritanceSimpleTask, DeadlineTask, RecurringTask inherit from Task
EncapsulationPrivate helpers and properties protect internal state
Polymorphismlist_tasks() works with any Task subclass
Modules/PackagesProject split into models/, manager.py, utils.py
File I/OJSON read/write for persistence
Exception handlingCorrupted files, missing keys, invalid inputs
ComprehensionsUseful for filtering tasks by status/priority

7. Phase 2 Complete! 🎉

You have now mastered:

DayTopic
8String Manipulation & Formatting
9File Handling
10Exception Handling & Debugging
11List Comprehensions & Lambda Functions
12Modules, Packages & Virtual Environments
13Intro to OOP — Classes & Objects
14OOP: Inheritance, Encapsulation, Polymorphism
15Phase 2 Capstone Project

Next stop: Phase 3 — Advanced Concepts (Days 16–23)

Day 16 kicks off with Iterators and Generators — memory‑efficient data processing and custom iteration.


🎯 Your Action Items for Day 15:

  1. ✅ Complete all 5 exercises
  2. ✅ Build the Full Task Manager application
  3. ✅ Experiment with adding new task types — see how easily the system extends
  4. ✅ Reflect on how OOP made this project more organised than procedural code would have

Comments

Popular posts from this blog

Day 1: Welcome to Python — Your Journey Begins

Python: Your Gateway to Coding Adventures

Day 11: List Comprehensions & Lambda Functions

Earn From the Comfort of Your Home

Build the skills to work comfortably from home, on your own terms.

Get Started Today