Home Quizzes Leaderboard Competitions Learn Hire Us
About Contact
Log In Sign Up
← 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.

View All Python Tutorials