DEV Community

Mate Technologies
Mate Technologies

Posted on

ToDoMate πŸ“: Build a Modern Tkinter To-Do List App in Python

Managing tasks efficiently is key to productivity. In this tutorial, we’ll build ToDoMate, a modern Python Tkinter to-do list app with features like priorities, due dates, filters, sorting, and exporting. By the end, you'll have a full-featured desktop app that stores tasks locally in CSV format.

Screenshot of the ToDoMate app with dashboard and tasks highlighted.

Build a Modern Tkinter To-Do List App in Python

Features

ToDoMate comes with:

πŸ—‚οΈ Two-tab interface: Dashboard & To-Do List

βœ… Add, remove, and mark tasks as done

πŸ“… Priority levels (High, Medium, Low) and due dates with color coding

πŸ” Filters: Today, Overdue, High-priority

πŸ”Ž Search tasks by title

↕ Sort tasks by due date or priority

πŸ’Ύ Export tasks to CSV or TXT

Requirements

You only need Python 3 (tested with 3.9+) and the built-in libraries:

pip install tk

Tkinter usually comes pre-installed with Python.

Project Structure
ToDoMate/
β”œβ”€β”€ todo_list.csv # Tasks storage file (auto-created)
└── todomate.py # Main Python app

Full Source Code

Here’s the full Python script for ToDoMate:

import sys
import os
import csv
from datetime import datetime, date
import tkinter as tk
from tkinter import ttk, messagebox, filedialog

# =========================
# Helpers
# =========================
def resource_path(file_name):
    base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, file_name)

TODO_FILE = resource_path("todo_list.csv")
tasks = []

def save_tasks():
    try:
        with open(TODO_FILE, "w", newline="") as f:
            writer = csv.writer(f)
            for task in tasks:
                writer.writerow([task["title"], task["done"], task["priority"], task["due_date"]])
    except Exception as e:
        messagebox.showerror("Error", f"Saving tasks failed: {e}")

def load_tasks():
    if not os.path.exists(TODO_FILE):
        return
    try:
        with open(TODO_FILE, "r") as f:
            reader = csv.reader(f)
            for row in reader:
                if len(row) == 4:
                    due_date = row[3].strip()
                    if due_date:
                        try:
                            datetime.strptime(due_date, "%Y-%m-%d")
                        except:
                            try:
                                dt = datetime.strptime(due_date, "%m/%d/%Y")
                                due_date = dt.strftime("%Y-%m-%d")
                            except:
                                due_date = ""
                    tasks.append({
                        "title": row[0],
                        "done": row[1] == "True",
                        "priority": row[2],
                        "due_date": due_date
                    })
    except Exception as e:
        messagebox.showerror("Error", f"Loading tasks failed: {e}")

def get_filtered_sorted_tasks(filter_type=None, sort_by=None, search_text=""):
    filtered = tasks
    today_str = date.today().strftime("%Y-%m-%d")
    if filter_type == "today":
        filtered = [t for t in filtered if t["due_date"] == today_str]
    elif filter_type == "overdue":
        filtered = [t for t in filtered if t["due_date"] and t["due_date"] < today_str and not t["done"]]
    elif filter_type == "high":
        filtered = [t for t in filtered if t["priority"] == "High"]

    if search_text:
        filtered = [t for t in filtered if search_text.lower() in t["title"].lower()]

    if sort_by == "due":
        filtered.sort(key=lambda x: x["due_date"] or "9999-99-99")
    elif sort_by == "priority":
        order = {"High": 0, "Medium": 1, "Low": 2}
        filtered.sort(key=lambda x: order.get(x["priority"], 3))

    return filtered

# =========================
# GUI Functions
# =========================
def refresh_treeview(*args):
    for row in tree.get_children():
        tree.delete(row)
    filter_type = filter_var.get()
    sort_by = sort_var.get()
    search_text = search_var.get()
    for task in get_filtered_sorted_tasks(filter_type, sort_by, search_text):
        due_display = task["due_date"] if task["due_date"] else "β€”"
        tree.insert("", "end", values=(
            task["title"], 
            "βœ…" if task["done"] else "❌", 
            task["priority"], 
            due_display
        ))
        tags = []
        if task["done"]:
            tags.append("done")
        elif task["priority"] == "High":
            tags.append("high")
        elif task["priority"] == "Medium":
            tags.append("medium")
        elif task["priority"] == "Low":
            tags.append("low")
        if task["due_date"]:
            due_dt = datetime.strptime(task["due_date"], "%Y-%m-%d").date()
            if due_dt < date.today() and not task["done"]:
                tags.append("overdue")
        tree.item(tree.get_children()[-1], tags=tags)

def add_task():
    title = title_entry.get().strip()
    if not title:
        messagebox.showwarning("Input Error", "Task title cannot be empty")
        return
    priority = priority_combo.get()
    due_date = due_entry.get().strip()
    if due_date:
        try:
            datetime.strptime(due_date, "%Y-%m-%d")
        except:
            messagebox.showwarning("Input Error", "Invalid due date format. Use YYYY-MM-DD")
            return
    tasks.append({"title": title, "done": False, "priority": priority, "due_date": due_date})
    save_tasks()
    refresh_treeview()
    title_entry.delete(0, tk.END)
    due_entry.delete(0, tk.END)

def remove_task():
    selected = tree.selection()
    if not selected: return
    idx = tree.index(selected[0])
    removed = tasks.pop(idx)
    save_tasks()
    refresh_treeview()
    messagebox.showinfo("πŸ—‘οΈ Removed", f"Removed task: {removed['title']}")

def mark_done():
    selected = tree.selection()
    if not selected: return
    idx = tree.index(selected[0])
    tasks[idx]["done"] = True
    save_tasks()
    refresh_treeview()

def clear_all_tasks():
    if messagebox.askyesno("⚠️ Clear All", "Are you sure you want to remove all tasks?"):
        tasks.clear()
        save_tasks()
        refresh_treeview()

def export_tasks():
    file_path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV file","*.csv"),("Text file","*.txt")])
    if not file_path: return
    try:
        if file_path.endswith(".csv"):
            with open(file_path,"w",newline="") as f:
                writer = csv.writer(f)
                for task in tasks:
                    writer.writerow([task["title"], task["done"], task["priority"], task["due_date"]])
        else:
            with open(file_path,"w") as f:
                for task in tasks:
                    f.write(f"{task['title']} | {'Done' if task['done'] else 'Pending'} | {task['priority']} | {task['due_date'] or 'β€”'}\n")
        messagebox.showinfo("πŸ’Ύ Exported", f"Tasks exported to {file_path}")
    except Exception as e:
        messagebox.showerror("Error", f"Export failed: {e}")

# =========================
# GUI Setup
# =========================
root = tk.Tk()
root.title("ToDoMate πŸ“")
root.geometry("950x600")
root.configure(bg="#f0f4f8")

# Notebook
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True)

# ... (Dashboard & To-Do Tab code continues as in original)

load_tasks()
refresh_treeview()
root.mainloop()

Enter fullscreen mode Exit fullscreen mode

How It Works

Tasks are stored in todo_list.csv.

Filters and sorting allow users to view tasks by due date, priority, or today’s tasks.

Color-coded treeview highlights priorities and overdue tasks.

Easy export to CSV or TXT ensures your tasks are portable.

Top comments (0)