Lesson 25 of 25

Final Project: Build a Task Manager

Project Overview and Data Layer

Let's put everything you've learned together by building a command-line Task Manager application. This project uses file handling, classes, error handling, and user input to create a complete CRUD (Create, Read, Update, Delete) application.

We'll store tasks in a JSON file so they persist between sessions. Each task will have an ID, title, description, status, and creation date.

  • Add new tasks with a title and description
  • View all tasks or filter by status (pending/completed)
  • Mark tasks as completed
  • Delete tasks
  • Data persists in a JSON file between runs
Example
import json
import os
from datetime import datetime

DATA_FILE = "tasks.json"

class Task:
    """Represents a single task."""

    def __init__(self, task_id, title, description="", completed=False, created_at=None):
        self.task_id = task_id
        self.title = title
        self.description = description
        self.completed = completed
        self.created_at = created_at or datetime.now().strftime("%Y-%m-%d %H:%M")

    def to_dict(self):
        """Convert task to a dictionary for JSON storage."""
        return {
            "id": self.task_id,
            "title": self.title,
            "description": self.description,
            "completed": self.completed,
            "created_at": self.created_at
        }

    @classmethod
    def from_dict(cls, data):
        """Create a Task from a dictionary."""
        return cls(
            task_id=data["id"],
            title=data["title"],
            description=data.get("description", ""),
            completed=data.get("completed", False),
            created_at=data.get("created_at")
        )

    def __str__(self):
        status = "Done" if self.completed else "Pending"
        return f"[{self.task_id}] [{status}] {self.title}"
Try the Task Class
JavaScript
# Simplified Task class demo
class Task:
    def __init__(self, task_id, title, completed=False):
        self.task_id = task_id
        self.title = title
        self.completed = completed

    def __str__(self):
        status = "Done" if self.completed else "Pending"
        return f"[{self.task_id}] [{status}] {self.title}"

    def to_dict(self):
        return {"id": self.task_id, "title": self.title, "completed": self.completed}

# Create tasks
tasks = [
    Task(1, "Learn Python basics", True),
    Task(2, "Build a project", False),
    Task(3, "Practice daily", False)
]

for task in tasks:
    print(task)
Notes
  • The @classmethod decorator creates an alternative constructor. Task.from_dict(data) creates a Task object from a dictionary, which is useful when loading from JSON.

Task Manager Class with CRUD Operations

The TaskManager class handles all operations: loading and saving tasks from the JSON file, adding new tasks, listing them, completing them, and deleting them.

This class encapsulates all the data management logic, keeping the main program loop clean and focused on user interaction.

Example
class TaskManager:
    """Manages a collection of tasks with file persistence."""

    def __init__(self, filepath=DATA_FILE):
        self.filepath = filepath
        self.tasks = []
        self.load_tasks()

    def load_tasks(self):
        """Load tasks from the JSON file."""
        if os.path.exists(self.filepath):
            try:
                with open(self.filepath, "r") as f:
                    data = json.load(f)
                self.tasks = [Task.from_dict(t) for t in data]
            except (json.JSONDecodeError, KeyError) as e:
                print(f"Warning: Could not load tasks ({e}). Starting fresh.")
                self.tasks = []
        else:
            self.tasks = []

    def save_tasks(self):
        """Save all tasks to the JSON file."""
        with open(self.filepath, "w") as f:
            json.dump([t.to_dict() for t in self.tasks], f, indent=2)

    def _next_id(self):
        """Generate the next available task ID."""
        if not self.tasks:
            return 1
        return max(t.task_id for t in self.tasks) + 1

    def add_task(self, title, description=""):
        """Add a new task and save."""
        task = Task(self._next_id(), title, description)
        self.tasks.append(task)
        self.save_tasks()
        print(f"Task added: {task}")
        return task

    def list_tasks(self, show_all=True):
        """Display tasks. If show_all is False, show only pending."""
        filtered = self.tasks if show_all else [t for t in self.tasks if not t.completed]
        if not filtered:
            print("No tasks found.")
            return
        for task in filtered:
            desc = f" - {task.description}" if task.description else ""
            print(f"  {task}{desc}")
        print(f"\n  Total: {len(filtered)} task(s)")

    def complete_task(self, task_id):
        """Mark a task as completed."""
        for task in self.tasks:
            if task.task_id == task_id:
                task.completed = True
                self.save_tasks()
                print(f"Completed: {task.title}")
                return
        print(f"Task #{task_id} not found.")

    def delete_task(self, task_id):
        """Delete a task by ID."""
        for i, task in enumerate(self.tasks):
            if task.task_id == task_id:
                removed = self.tasks.pop(i)
                self.save_tasks()
                print(f"Deleted: {removed.title}")
                return
        print(f"Task #{task_id} not found.")
Try TaskManager Operations
JavaScript
# Simulated TaskManager demo (without file I/O)
class Task:
    def __init__(self, tid, title, done=False):
        self.tid = tid
        self.title = title
        self.done = done
    def __str__(self):
        s = "Done" if self.done else "Pending"
        return f"[{self.tid}] [{s}] {self.title}"

tasks = []
next_id = 1

def add(title):
    global next_id
    t = Task(next_id, title)
    tasks.append(t)
    next_id += 1
    print(f"Added: {t}")

add("Learn Python")
add("Build a project")
add("Get hired")

tasks[0].done = True
print("\nAll tasks:")
for t in tasks:
    print(f"  {t}")
Notes
  • The save_tasks() method is called after every modification to ensure data persists. In a larger application, you might batch writes for performance.

The Main Program Loop

The main function ties everything together with a menu-driven interface. It uses a while loop to keep the program running until the user chooses to quit, and handles invalid input gracefully.

This is the complete entry point. Save all three sections into a single file (task_manager.py) and run it with 'python task_manager.py'.

Example
def main():
    """Main application loop."""
    manager = TaskManager()
    print("=== Task Manager ===")
    print("Type 'help' for available commands.\n")

    while True:
        command = input("\n> ").strip().lower()

        if command == "help":
            print("Commands:")
            print("  add      — Add a new task")
            print("  list     — Show all tasks")
            print("  pending  — Show pending tasks only")
            print("  done ID  — Mark task as completed")
            print("  delete ID— Delete a task")
            print("  quit     — Exit the program")

        elif command == "add":
            title = input("Task title: ").strip()
            if not title:
                print("Title cannot be empty.")
                continue
            desc = input("Description (optional): ").strip()
            manager.add_task(title, desc)

        elif command == "list":
            manager.list_tasks(show_all=True)

        elif command == "pending":
            manager.list_tasks(show_all=False)

        elif command.startswith("done "):
            try:
                task_id = int(command.split()[1])
                manager.complete_task(task_id)
            except (IndexError, ValueError):
                print("Usage: done <task_id>")

        elif command.startswith("delete "):
            try:
                task_id = int(command.split()[1])
                manager.delete_task(task_id)
            except (IndexError, ValueError):
                print("Usage: delete <task_id>")

        elif command in ("quit", "exit", "q"):
            print("Goodbye!")
            break

        else:
            print("Unknown command. Type 'help' for options.")

if __name__ == "__main__":
    main()
Try the Complete Project
JavaScript
# Complete Task Manager — save as task_manager.py
# and run with: python task_manager.py
#
# Sample session:
# > add
# Task title: Learn Python
# Description: Complete all 25 lessons
# Task added: [1] [Pending] Learn Python
#
# > add
# Task title: Build project
# Description: Create task manager app
# Task added: [2] [Pending] Build project
#
# > list
#   [1] [Pending] Learn Python - Complete all 25 lessons
#   [2] [Pending] Build project - Create task manager app
#   Total: 2 task(s)
#
# > done 1
# Completed: Learn Python
#
# > list
#   [1] [Done] Learn Python - Complete all 25 lessons
#   [2] [Pending] Build project - Create task manager app
#   Total: 2 task(s)
#
# > quit
# Goodbye!
Notes
  • Congratulations on completing the Python course! You've learned variables, data types, control flow, data structures, functions, OOP, file handling, error handling, decorators, APIs, and best practices. Keep building projects to solidify your skills!