Complete Project 1: Todo List Application

Building a Todo List app is like creating your own digital notebook for tasks. It's the 'Hello World' of real applications - simple enough for beginners but comprehensive enough to use most programming concepts.
The Todo List application is a complete, real‑world project that brings together many of the Python concepts you've learned: file handling (JSON), user input, data structures (lists and dictionaries), functions, datetime handling, error handling, and object‑oriented programming. It's a console‑based task manager that demonstrates how to build a useful, persistent application from scratch.

**Core Features:**
- Add tasks with title, description, priority (1‑3), due date, and category.
- View all tasks, pending tasks only, or completed tasks only.
- Mark tasks as complete (stores completion timestamp).
- Delete tasks (with confirmation and automatic ID renumbering).
- Search tasks by keyword (title, description, or category).
- Statistics (total, completed, pending, completion rate, breakdown by priority and category, overdue count).
- Data persistence using a JSON file (`todos.json`).

**How It Works:**

The `TodoApp` class is the heart of the application. Its `__init__` method loads existing tasks from `todos.json` using `load_todos()`. If the file doesn't exist or is corrupted, it starts with an empty list.

Each task is stored as a dictionary with keys: `id`, `title`, `description`, `priority`, `due_date`, `category`, `completed`, `created_at`, and `completed_at`. This structured format makes it easy to save to JSON and reload.

**Adding a Task (`add_task`):**
- Prompts the user for title (required), description (optional), priority (with validation), due date (with date format validation), and category (defaults to "general").
- Generates a new ID based on current list length + 1.
- Appends the task to `self.todos` and calls `save_todos()` to write to disk.

**Viewing Tasks (`view_tasks`):**
- Accepts an optional `show_completed` parameter: `None` for all tasks, `True` for completed only, `False` for pending only.
- Filters the list accordingly and displays each task using `display_task`.

**Displaying a Task (`display_task`):**
- Shows priority symbols (⚪ low, 🟡 medium, 🔴 high).
- Shows status (✅ completed, ⏳ pending).
- Shows category (uppercase).
- For pending tasks with a due date, checks if the date has passed and marks it as `⚠ OVERDUE!`.
- Shows completion timestamp if completed.

**Marking Complete (`mark_complete`):**
- Lists all pending tasks with their IDs.
- User selects an ID, and the task's `completed` flag is set to `True`, and `completed_at` is set to the current datetime.
- Saves changes.

**Deleting a Task (`delete_task`):**
- Lists all tasks with their IDs and status icons.
- User selects an ID and confirms deletion.
- The task is removed from the list, and then all remaining tasks are renumbered sequentially (IDs become 1, 2, 3…).
- Saves changes.

**Searching (`search_tasks`):**
- Takes a keyword and searches in `title`, `description`, and `category` (case‑insensitive).
- Displays matching tasks.

**Statistics (`show_statistics`):**
- Calculates total, completed, pending, completion rate.
- Counts tasks by priority and category.
- Counts overdue tasks (pending tasks with due date before today).

**Error Handling:**
- Input validation for priority (must be 1‑3) and due date (must match YYYY‑MM‑DD).
- File I/O errors are caught when loading/saving (shows a warning but doesn't crash).
- The main loop catches `KeyboardInterrupt` (Ctrl+C) to exit gracefully.

**Why This Project Is Important:**
- It uses **JSON** for data storage, which is human‑readable and easy to debug.
- It demonstrates a **menu‑driven** console interface – a common pattern for command‑line tools.
- It shows how to manage **state** (list of tasks) and **persist** it.
- It practices **defensive programming** (validation, error messages, confirmations).
- It is **extensible** – you could easily add features like editing tasks, sorting, or exporting to CSV.

**How to Run:**
Save the code as `todo.py` and run `python todo.py`. The first time, it will create `todos.json`. All your tasks are saved automatically after every change.

**Potential Enhancements (for practice):**
- Add an `edit_task` function to modify title, description, priority, etc.
- Sort tasks by due date or priority when viewing.
- Add a due date reminder that warns at program start.
- Export tasks to CSV or Markdown.
- Add a simple password to protect the todo list.

This project is a milestone: you have built a complete, useful application that you can actually use every day. It demonstrates that you can now take Python beyond simple scripts and create interactive, data‑driven programs.
# TODO LIST APPLICATION
import json
import os
from datetime import datetime

print("TODO LIST APPLICATION")
print("=" * 50)

class TodoApp:
    """A simple Todo List Application"""
    
    def __init__(self, filename="todos.json"):
        self.filename = filename
        self.todos = self.load_todos()
    
    def load_todos(self):
        """Load todos from JSON file"""
        if os.path.exists(self.filename):
            try:
                with open(self.filename, 'r') as file:
                    return json.load(file)
            except (json.JSONDecodeError, IOError):
                print("Warning: Could not load todos. Starting fresh.")
                return []
        return []
    
    def save_todos(self):
        """Save todos to JSON file"""
        try:
            with open(self.filename, 'w') as file:
                json.dump(self.todos, file, indent=2)
        except IOError:
            print("Error: Could not save todos to file.")
    
    def display_menu(self):
        """Display main menu"""
        print("\n" + "=" * 40)
        print("TODO LIST MENU")
        print("=" * 40)
        print("1. View All Tasks")
        print("2. View Pending Tasks")
        print("3. View Completed Tasks")
        print("4. Add New Task")
        print("5. Mark Task as Complete")
        print("6. Delete Task")
        print("7. Search Tasks")
        print("8. Statistics")
        print("9. Exit")
        print("=" * 40)
    
    def add_task(self):
        """Add a new task"""
        print("\n" + "-" * 30)
        print("ADD NEW TASK")
        print("-" * 30)
        
        title = input("Task title: ").strip()
        if not title:
            print("Task title cannot be empty!")
            return
        
        description = input("Task description (optional): ").strip()
        priority = self.get_priority()
        due_date = self.get_due_date()
        category = input("Category (work/personal/health/etc): ").strip().lower() or "general"
        
        new_task = {
            "id": len(self.todos) + 1,
            "title": title,
            "description": description,
            "priority": priority,
            "due_date": due_date,
            "category": category,
            "completed": False,
            "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "completed_at": None
        }
        
        self.todos.append(new_task)
        self.save_todos()
        print(f"\n✅ Task '{title}' added successfully!")
    
    def get_priority(self):
        """Get task priority from user"""
        while True:
            try:
                priority = input("Priority (1=Low, 2=Medium, 3=High): ").strip()
                if not priority:
                    return 2  # Default medium
                
                priority = int(priority)
                if 1 <= priority <= 3:
                    return priority
                else:
                    print("Please enter 1, 2, or 3")
            except ValueError:
                print("Please enter a valid number")
    
    def get_due_date(self):
        """Get due date from user"""
        while True:
            due_date = input("Due date (YYYY-MM-DD or press Enter for none): ").strip()
            if not due_date:
                return None
            
            try:
                # Validate date format
                datetime.strptime(due_date, "%Y-%m-%d")
                return due_date
            except ValueError:
                print("Invalid date format. Please use YYYY-MM-DD")
    
    def view_tasks(self, show_completed=None):
        """View tasks based on completion status"""
        if show_completed is None:
            tasks = self.todos
            title = "ALL TASKS"
        elif show_completed:
            tasks = [t for t in self.todos if t["completed"]]
            title = "COMPLETED TASKS"
        else:
            tasks = [t for t in self.todos if not t["completed"]]
            title = "PENDING TASKS"
        
        if not tasks:
            print(f"\nNo {title.lower()} found.")
            return
        
        print(f"\n{title}")
        print("-" * 60)
        
        for task in tasks:
            self.display_task(task)
        
        print(f"\nTotal: {len(tasks)} task(s)")
    
    def display_task(self, task):
        """Display a single task"""
        # Priority indicators
        priority_symbols = {1: "⚪", 2: "🟡", 3: "🔴"}
        priority_symbol = priority_symbols.get(task["priority"], "⚪")
        
        # Completion status
        status = "✅" if task["completed"] else "⏳"
        
        # Format display
        print(f"\n{status} {priority_symbol} Task #{task['id']}: {task['title']}")
        
        if task["description"]:
            print(f"   Description: {task['description']}")
        
        print(f"   Category: {task['category'].upper()}")
        
        if task["due_date"]:
            due_text = f"Due: {task['due_date']}"
            # Check if overdue (only for pending tasks)
            if not task["completed"] and task["due_date"]:
                due_date = datetime.strptime(task["due_date"], "%Y-%m-%d")
                today = datetime.now().date()
                if due_date.date() < today:
                    due_text += " ⚠ OVERDUE!"
            print(f"   {due_text}")
        
        if task["completed"] and task["completed_at"]:
            print(f"   Completed: {task['completed_at']}")
    
    def mark_complete(self):
        """Mark a task as complete"""
        pending_tasks = [t for t in self.todos if not t["completed"]]
        if not pending_tasks:
            print("\nNo pending tasks to complete!")
            return
        
        print("\nPENDING TASKS:")
        for task in pending_tasks:
            print(f"{task['id']}. {task['title']}")
        
        try:
            task_id = int(input("\nEnter task ID to mark as complete: "))
            for task in self.todos:
                if task["id"] == task_id and not task["completed"]:
                    task["completed"] = True
                    task["completed_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    self.save_todos()
                    print(f"\n✅ Task '{task['title']}' marked as complete!")
                    return
            print("\n❌ Task not found or already completed.")
        except ValueError:
            print("\n❌ Please enter a valid number.")
    
    def delete_task(self):
        """Delete a task"""
        if not self.todos:
            print("\nNo tasks to delete!")
            return
        
        print("\nALL TASKS:")
        for task in self.todos:
            status = "✅" if task["completed"] else "⏳"
            print(f"{task['id']}. {status} {task['title']}")
        
        try:
            task_id = int(input("\nEnter task ID to delete: "))
            for i, task in enumerate(self.todos):
                if task["id"] == task_id:
                    confirm = input(f"Are you sure you want to delete '{task['title']}'? (y/n): ")
                    if confirm.lower() == 'y':
                        deleted_task = self.todos.pop(i)
                        self.save_todos()
                        print(f"\n🗑 Task '{deleted_task['title']}' deleted successfully!")
                        # Reassign IDs
                        for j, t in enumerate(self.todos, 1):
                            t["id"] = j
                        self.save_todos()
                    return
            print("\n❌ Task not found.")
        except ValueError:
            print("\n❌ Please enter a valid number.")
    
    def search_tasks(self):
        """Search tasks by keyword"""
        if not self.todos:
            print("\nNo tasks to search!")
            return
        
        keyword = input("\nEnter search keyword: ").lower().strip()
        if not keyword:
            print("Please enter a keyword to search.")
            return
        
        results = []
        for task in self.todos:
            if (keyword in task["title"].lower() or 
                keyword in task["description"].lower() or
                keyword in task["category"].lower()):
                results.append(task)
        
        if results:
            print(f"\n📊 SEARCH RESULTS FOR '{keyword}':")
            print("-" * 50)
            for task in results:
                self.display_task(task)
            print(f"\nFound {len(results)} task(s)")
        else:
            print(f"\nNo tasks found matching '{keyword}'.")
    
    def show_statistics(self):
        """Show task statistics"""
        if not self.todos:
            print("\nNo tasks to show statistics for!")
            return
        
        total = len(self.todos)
        completed = sum(1 for t in self.todos if t["completed"])
        pending = total - completed
        
        # Calculate completion percentage
        completion_rate = (completed / total * 100) if total > 0 else 0
        
        # Tasks by priority
        high_priority = sum(1 for t in self.todos if t["priority"] == 3)
        medium_priority = sum(1 for t in self.todos if t["priority"] == 2)
        low_priority = sum(1 for t in self.todos if t["priority"] == 1)
        
        # Tasks by category
        categories = {}
        for task in self.todos:
            cat = task["category"]
            categories[cat] = categories.get(cat, 0) + 1
        
        print("\n" + "📈 TASK STATISTICS")
        print("=" * 40)
        print(f"Total Tasks: {total}")
        print(f"Completed: {completed}")
        print(f"Pending: {pending}")
        print(f"Completion Rate: {completion_rate:.1f}%")
        
        print("\nBy Priority:")
        print(f"  🔴 High: {high_priority}")
        print(f"  🟡 Medium: {medium_priority}")
        print(f"  ⚪ Low: {low_priority}")
        
        print("\nBy Category:")
        for category, count in categories.items():
            print(f"  {category.upper()}: {count}")
        
        # Overdue tasks
        overdue = 0
        today = datetime.now().date()
        for task in self.todos:
            if not task["completed"] and task["due_date"]:
                due_date = datetime.strptime(task["due_date"], "%Y-%m-%d").date()
                if due_date < today:
                    overdue += 1
        
        if overdue > 0:
            print(f"\n⚠ Overdue Tasks: {overdue}")
    
    def run(self):
        """Run the todo application"""
        print("\n✨ Welcome to the Todo List App!")
        print("Manage your tasks efficiently.")
        
        while True:
            self.display_menu()
            
            try:
                choice = input("\nEnter your choice (1-9): ").strip()
                
                if choice == "1":
                    self.view_tasks()
                elif choice == "2":
                    self.view_tasks(show_completed=False)
                elif choice == "3":
                    self.view_tasks(show_completed=True)
                elif choice == "4":
                    self.add_task()
                elif choice == "5":
                    self.mark_complete()
                elif choice == "6":
                    self.delete_task()
                elif choice == "7":
                    self.search_tasks()
                elif choice == "8":
                    self.show_statistics()
                elif choice == "9":
                    print("\n👋 Thank you for using Todo List App!")
                    print("Your tasks have been saved.")
                    break
                else:
                    print("\n❌ Invalid choice. Please enter 1-9.")
            except KeyboardInterrupt:
                print("\n\n👋 Goodbye!")
                break
            except Exception as e:
                print(f"\n❌ An error occurred: {e}")

# Run the application
if __name__ == "__main__":
    app = TodoApp()
    app.run()

→ Run this code interactively