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.

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=".cs v", 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()
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.
More...
Screenshot of the ToDoMate app with dashboard and tasks highlighted.

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=".cs v", 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()
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.
More...