Complete Project 1: Todo List Application

This project combines file handling (saving tasks), user input, data structures (lists/dictionaries), functions, and basic error handling. We'll create a console-based application that lets users add, view, complete, and delete tasks, with data persistence to a JSON file.

# 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()