← Back to Learn
Beginner
Python
• 2-3 hours
Python Todo App
Build a task management application. NOTE: This is a base for you to create someething wonderful.
🛠️ Tech Stack
Python
Flask
💻 Code Example
import tkinter as tk
from tkinter import font as tkfont
import json, os
from datetime import datetime
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "todos.json")
# ── Palette: warm cream luxury ─────────────────────────────────────────────────
C = {
"bg": "#FAF9F7",
"surface": "#FFFFFF",
"surface2": "#F5F3EF",
"border": "#E8E4DC",
"border2": "#C8C3B8",
"ink": "#1A1916",
"ink2": "#6B6760",
"ink3": "#B0ADA7",
"done_text": "#C4C0BA",
"hover": "#F4F2EE",
"tag_high": "#B83232",
"tag_med": "#A07000",
"tag_low": "#2A6E48",
"tag_high_bg": "#FDF2F2",
"tag_med_bg": "#FDF8EC",
"tag_low_bg": "#F0FAF4",
"scrollbar": "#DDD9D2",
"accent_fill": "#1A1916",
"accent_text": "#FAF9F7",
}
PRI_COLOR = {"high": C["tag_high"], "medium": C["tag_med"], "low": C["tag_low"]}
PRI_BG = {"high": C["tag_high_bg"], "medium": C["tag_med_bg"], "low": C["tag_low_bg"]}
PRI_DOT = {"high": "●", "medium": "●", "low": "●"}
PRI_LABELS = ["High", "Medium", "Low"]
PRI_KEY = {"High": "high", "Medium": "medium", "Low": "low"}
def load_todos():
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE) as f:
return json.load(f)
except Exception:
pass
return []
def save_todos(todos):
with open(DATA_FILE, "w") as f:
json.dump(todos, f, indent=2)
def _repaint(widget, color):
try:
widget.config(bg=color)
except Exception:
pass
for child in widget.winfo_children():
_repaint(child, color)
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Tasks")
self.geometry("740x720")
self.minsize(560, 500)
self.configure(bg=C["bg"])
self.resizable(True, True)
self.todos = load_todos()
self.filter_var = tk.StringVar(value="all")
self.search_var = tk.StringVar()
self.pri_var = tk.StringVar(value="Medium")
self.search_var.trace_add("write", lambda *_: self.refresh())
self._fonts()
self._build()
self.refresh()
# ── Fonts ──────────────────────────────────────────────────────────────────
def _fonts(self):
self.F = {
"title": tkfont.Font(family="Georgia", size=24, weight="bold"),
"serif": tkfont.Font(family="Georgia", size=13),
"body": tkfont.Font(family="Helvetica Neue", size=12),
"body_s": tkfont.Font(family="Helvetica Neue", size=11),
"small": tkfont.Font(family="Helvetica Neue", size=10),
"label": tkfont.Font(family="Helvetica Neue", size=10, weight="bold"),
"strike": tkfont.Font(family="Helvetica Neue", size=12, overstrike=True),
"mono": tkfont.Font(family="Menlo", size=9),
}
# ── Build ──────────────────────────────────────────────────────────────────
def _build(self):
PAD = 48 # horizontal padding
# ── Header ────────────────────────────────────────────────────────────
hdr = tk.Frame(self, bg=C["bg"])
hdr.pack(fill="x", padx=PAD, pady=(40, 0))
tk.Label(hdr, text="Tasks", font=self.F["title"],
bg=C["bg"], fg=C["ink"]).pack(side="left", anchor="s")
self.stats_var = tk.StringVar()
tk.Label(hdr, textvariable=self.stats_var, font=self.F["small"],
bg=C["bg"], fg=C["ink3"]).pack(
side="left", anchor="s", padx=(14, 0))
# ── Progress hairline ─────────────────────────────────────────────────
self.prog_canvas = tk.Canvas(self, height=2, bg=C["border"],
highlightthickness=0)
self.prog_canvas.pack(fill="x", padx=PAD, pady=(18, 0))
# ── Add task card ─────────────────────────────────────────────────────
add_wrap = tk.Frame(self, bg=C["surface"],
highlightthickness=1,
highlightbackground=C["border2"])
add_wrap.pack(fill="x", padx=PAD, pady=(22, 0))
# Input row
inp_row = tk.Frame(add_wrap, bg=C["surface"])
inp_row.pack(fill="x", padx=20, pady=(14, 0))
self.task_entry = tk.Entry(
inp_row, font=self.F["body"],
bg=C["surface"], fg=C["ink3"],
insertbackground=C["ink"],
relief="flat", bd=0)
self.task_entry.insert(0, "What needs to be done?")
self.task_entry.pack(side="left", fill="x", expand=True, ipady=2)
self.task_entry.bind("<FocusIn>", self._ph_in)
self.task_entry.bind("<FocusOut>", self._ph_out)
self.task_entry.bind("<Return>", lambda e: self._add())
self.add_btn = tk.Label(inp_row, text="Add →",
font=self.F["label"],
bg=C["accent_fill"], fg=C["accent_text"],
padx=16, pady=7, cursor="hand2")
self.add_btn.pack(side="right")
self.add_btn.bind("<Button-1>", lambda e: self._add())
self.add_btn.bind("<Enter>", lambda e: self.add_btn.config(bg=C["ink2"]))
self.add_btn.bind("<Leave>", lambda e: self.add_btn.config(bg=C["ink"]))
# Divider
tk.Frame(add_wrap, bg=C["border"], height=1).pack(
fill="x", padx=20, pady=(10, 0))
# Priority pills row
pill_row = tk.Frame(add_wrap, bg=C["surface"])
pill_row.pack(fill="x", padx=20, pady=(10, 14))
tk.Label(pill_row, text="Priority", font=self.F["small"],
bg=C["surface"], fg=C["ink3"]).pack(side="left", padx=(0, 12))
self.pills = {}
for p in PRI_LABELS:
pill = tk.Label(pill_row, text=p, font=self.F["label"],
cursor="hand2", padx=12, pady=4)
pill.pack(side="left", padx=(0, 6))
pill.bind("<Button-1>", lambda e, pp=p: self._pick_pri(pp))
self.pills[p] = pill
self._pick_pri("Medium")
# ── Filters + search ──────────────────────────────────────────────────
ctrl = tk.Frame(self, bg=C["bg"])
ctrl.pack(fill="x", padx=PAD, pady=(20, 0))
self.filter_btns = {}
tabs = tk.Frame(ctrl, bg=C["bg"])
tabs.pack(side="left")
for lbl, mode in [("All", "all"), ("Active", "active"), ("Done", "done")]:
btn = tk.Label(tabs, text=lbl, font=self.F["small"],
fg=C["ink3"], bg=C["bg"],
padx=0, pady=4, cursor="hand2")
btn.pack(side="left", padx=(0, 20))
btn.bind("<Button-1>", lambda e, m=mode: self._filter(m))
self.filter_btns[mode] = btn
# Search
srch = tk.Frame(ctrl, bg=C["surface"],
highlightthickness=1, highlightbackground=C["border"])
srch.pack(side="right")
tk.Label(srch, text="⌕", font=self.F["body_s"],
bg=C["surface"], fg=C["ink3"]).pack(side="left", padx=(8, 2))
tk.Entry(srch, textvariable=self.search_var, font=self.F["small"],
bg=C["surface"], fg=C["ink"],
insertbackground=C["ink"],
relief="flat", bd=4, width=16).pack(side="left")
# ── Divider ───────────────────────────────────────────────────────────
tk.Frame(self, bg=C["border"], height=1).pack(
fill="x", padx=PAD, pady=(14, 0))
# ── Task list (scrollable) ────────────────────────────────────────────
list_wrap = tk.Frame(self, bg=C["bg"])
list_wrap.pack(fill="both", expand=True, padx=PAD, pady=(0, 0))
self.canvas = tk.Canvas(list_wrap, bg=C["bg"],
highlightthickness=0, bd=0)
sb = tk.Scrollbar(list_wrap, orient="vertical",
command=self.canvas.yview,
width=5, troughcolor=C["bg"],
bg=C["scrollbar"], relief="flat")
self.list_frame = tk.Frame(self.canvas, bg=C["bg"])
self.list_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")))
self._win_id = self.canvas.create_window(
(0, 0), window=self.list_frame, anchor="nw")
self.canvas.configure(yscrollcommand=sb.set)
self.canvas.pack(side="left", fill="both", expand=True)
sb.pack(side="right", fill="y")
self.canvas.bind("<Configure>",
lambda e: self.canvas.itemconfig(self._win_id, width=e.width))
self.canvas.bind("<MouseWheel>",
lambda e: self.canvas.yview_scroll(-1*(e.delta//120), "units"))
# ── Footer ────────────────────────────────────────────────────────────
tk.Frame(self, bg=C["border"], height=1).pack(
fill="x", padx=PAD, pady=(4, 0))
foot = tk.Frame(self, bg=C["bg"])
foot.pack(fill="x", padx=PAD, pady=(8, 20))
clr = tk.Label(foot, text="Clear completed",
font=self.F["small"], fg=C["ink3"],
bg=C["bg"], cursor="hand2")
clr.pack(side="right")
clr.bind("<Button-1>", lambda e: self._clear())
clr.bind("<Enter>", lambda e: clr.config(fg=C["tag_high"]))
clr.bind("<Leave>", lambda e: clr.config(fg=C["ink3"]))
# ── Placeholder ────────────────────────────────────────────────────────────
PH = "What needs to be done?"
def _ph_in(self, e):
if self.task_entry.get() == self.PH:
self.task_entry.delete(0, "end")
self.task_entry.config(fg=C["ink"])
def _ph_out(self, e):
if not self.task_entry.get():
self.task_entry.insert(0, self.PH)
self.task_entry.config(fg=C["ink3"])
# ── Priority pills ─────────────────────────────────────────────────────────
def _pick_pri(self, p):
self.pri_var.set(p)
for label, pill in self.pills.items():
k = PRI_KEY[label]
if label == p:
pill.config(fg=PRI_COLOR[k], bg=PRI_BG[k],
highlightthickness=1,
highlightbackground=PRI_COLOR[k])
else:
pill.config(fg=C["ink3"], bg=C["surface"],
highlightthickness=1,
highlightbackground=C["border"])
# ── Filter ─────────────────────────────────────────────────────────────────
def _filter(self, mode):
self.filter_var.set(mode)
self.refresh()
# ── CRUD ───────────────────────────────────────────────────────────────────
def _add(self):
text = self.task_entry.get().strip()
if not text or text == self.PH:
return
self.todos.append({
"id": int(datetime.now().timestamp() * 1000),
"text": text,
"done": False,
"priority": PRI_KEY[self.pri_var.get()],
"created": datetime.now().strftime("%d %b"),
})
save_todos(self.todos)
self.task_entry.delete(0, "end")
self.task_entry.insert(0, self.PH)
self.task_entry.config(fg=C["ink3"])
self.refresh()
def _toggle(self, tid):
for t in self.todos:
if t["id"] == tid:
t["done"] = not t["done"]
save_todos(self.todos)
self.refresh()
def _delete(self, tid):
self.todos = [t for t in self.todos if t["id"] != tid]
save_todos(self.todos)
self.refresh()
def _clear(self):
self.todos = [t for t in self.todos if not t["done"]]
save_todos(self.todos)
self.refresh()
# ── Render ─────────────────────────────────────────────────────────────────
def refresh(self):
for w in self.list_frame.winfo_children():
w.destroy()
mode = self.filter_var.get()
q = self.search_var.get().strip().lower()
# Update filter tab styles
for m, btn in self.filter_btns.items():
if m == mode:
btn.config(fg=C["ink"],
font=tkfont.Font(family="Helvetica Neue",
size=10, weight="bold"))
else:
btn.config(fg=C["ink3"], font=self.F["small"])
total = len(self.todos)
done = sum(1 for t in self.todos if t["done"])
rem = total - done
self.stats_var.set(
f"{rem} remaining · {done} done")
# Progress bar
self.prog_canvas.update_idletasks()
W = self.prog_canvas.winfo_width() or 650
self.prog_canvas.delete("all")
self.prog_canvas.config(bg=C["border"])
if total > 0 and done > 0:
self.prog_canvas.create_rectangle(
0, 0, int(W * done / total), 2,
fill=C["ink"], outline="")
visible = [
t for t in self.todos
if (mode == "all"
or (mode == "active" and not t["done"])
or (mode == "done" and t["done"]))
and (not q or q in t["text"].lower())
]
pw = {"high": 0, "medium": 1, "low": 2}
visible.sort(key=lambda t: (t["done"], pw.get(t["priority"], 1)))
if not visible:
msg = ("No tasks yet.\nAdd your first task above."
if not self.todos else "No tasks match.")
tk.Label(self.list_frame, text=msg, font=self.F["body"],
bg=C["bg"], fg=C["ink3"],
justify="center").pack(pady=60)
return
for i, todo in enumerate(visible):
self._row(todo, i, len(visible))
# ── Task row ───────────────────────────────────────────────────────────────
def _row(self, todo, idx, total):
done = todo["done"]
pri = todo["priority"]
row = tk.Frame(self.list_frame, bg=C["bg"])
row.pack(fill="x")
def enter(e):
_repaint(row, C["hover"])
def leave(e):
_repaint(row, C["bg"])
for w in (row,):
w.bind("<Enter>", enter)
w.bind("<Leave>", leave)
inner = tk.Frame(row, bg=C["bg"])
inner.pack(fill="x", pady=0)
inner.bind("<Enter>", enter)
inner.bind("<Leave>", leave)
# Checkbox (canvas drawn circle)
S = 18
chk = tk.Canvas(inner, width=S, height=S, bg=C["bg"],
highlightthickness=0, cursor="hand2")
chk.pack(side="left", padx=(0, 16), pady=15)
chk.bind("<Enter>", enter)
chk.bind("<Leave>", leave)
chk.bind("<Button-1>", lambda e, tid=todo["id"]: self._toggle(tid))
if done:
chk.create_oval(1, 1, S-1, S-1,
fill=C["ink"], outline=C["ink"])
# checkmark
chk.create_line(5, S//2, S//2-1, S-5,
fill=C["bg"], width=1.5, capstyle="round",
joinstyle="round")
chk.create_line(S//2-1, S-5, S-4, 4,
fill=C["bg"], width=1.5, capstyle="round",
joinstyle="round")
else:
chk.create_oval(1, 1, S-1, S-1,
fill="", outline=C["border2"], width=1.2)
# Task text
tf = self.F["strike"] if done else self.F["body"]
tc = C["done_text"] if done else C["ink"]
lbl = tk.Label(inner, text=todo["text"],
font=tf, fg=tc, bg=C["bg"],
anchor="w", justify="left", wraplength=330)
lbl.pack(side="left", fill="x", expand=True)
lbl.bind("<Enter>", enter)
lbl.bind("<Leave>", leave)
# Right meta
meta = tk.Frame(inner, bg=C["bg"])
meta.pack(side="right", padx=(8, 0))
meta.bind("<Enter>", enter)
meta.bind("<Leave>", leave)
# Priority badge
bgl = tk.Label(meta,
text=f" {PRI_DOT[pri]} {pri.capitalize()} ",
font=self.F["label"],
fg=PRI_COLOR[pri], bg=PRI_BG[pri],
pady=3)
bgl.pack(side="left", padx=(0, 12))
# Date
tk.Label(meta, text=todo.get("created", ""),
font=self.F["mono"], fg=C["ink3"],
bg=C["bg"]).pack(side="left", padx=(0, 14))
# Delete ✕
del_lbl = tk.Label(meta, text="✕",
font=self.F["small"], fg=C["ink3"],
bg=C["bg"], cursor="hand2", padx=4)
del_lbl.pack(side="left", padx=(0, 2))
del_lbl.bind("<Button-1>", lambda e, tid=todo["id"]: self._delete(tid))
del_lbl.bind("<Enter>",
lambda e: del_lbl.config(fg=C["tag_high"]))
del_lbl.bind("<Leave>",
lambda e: del_lbl.config(fg=C["ink3"]))
# Thin separator
if idx < total - 1:
sep = tk.Frame(self.list_frame, bg=C["border"], height=1)
sep.pack(fill="x")
if __name__ == "__main__":
app = App()
app.mainloop()
🔗 Resources
No external resources available for this project.