Introduction
A to-do list is the classic first CRUD app, and Streamlit makes it a satisfying weekend build โ you get a real web interface with checkboxes and buttons in under 80 lines of Python. If you have already built the age calculator, this is the natural next step because it introduces *state that changes over time*, not just state that responds to widgets.
The interesting problem here is persistence: a Streamlit rerun wipes plain variables, so the app stores tasks in a JSON file on disk and reloads them on every rerun. That one design decision turns a demo into a tool you can actually keep using.
Features
Prerequisites
pip install streamlitStep 1: Create the Script
Create a file called todo_app.py:
import streamlit as st
import json
import os
from datetime import date
TASKS_FILE = "tasks.json"
def load_tasks():
if os.path.exists(TASKS_FILE):
with open(TASKS_FILE, "r") as f:
return json.load(f)
return []
def save_tasks(tasks):
with open(TASKS_FILE, "w") as f:
json.dump(tasks, f, indent=2)
st.set_page_config(page_title="To-Do List", page_icon="โ
")
st.title("โ
My To-Do List")
tasks = load_tasks()
# Add new task
new_task = st.text_input("New task", placeholder="What needs to be done?")
if st.button("Add Task") and new_task.strip():
tasks.append({"task": new_task.strip(), "done": False, "added": str(date.today())})
save_tasks(tasks)
st.rerun()
# Progress
if tasks:
done_count = sum(1 for t in tasks if t["done"])
st.progress(done_count / len(tasks), text=f"{done_count} of {len(tasks)} tasks complete")
# Render tasks
for i, t in enumerate(tasks):
col_check, col_label, col_del = st.columns([0.08, 0.82, 0.10])
with col_check:
done = st.checkbox("", value=t["done"], key=f"check_{i}")
with col_label:
if done:
st.markdown(f":gray[~~{t['task']}~~]")
else:
st.markdown(t["task"])
with col_del:
if st.button("๐", key=f"del_{i}"):
tasks.pop(i)
save_tasks(tasks)
st.rerun()
if done != t["done"]:
tasks[i]["done"] = done
save_tasks(tasks)
st.rerun()
else:
st.info("No tasks yet โ add your first one above!")Step 2: Run the App
streamlit run todo_app.pyThe app opens in your browser. Add a task, tick it off, delete another โ then refresh the page and watch everything survive.
How It Works
The app follows a load โ modify โ save โ rerun cycle. Every interaction reads the JSON file into a Python list, mutates it, writes it back, and calls st.rerun() to refresh the UI immediately. Without the explicit rerun, the interface would lag one interaction behind the file โ a direct consequence of Streamlit's top-to-bottom script model.
Checkboxes are the subtle part. Each checkbox gets a unique key so Streamlit can track its state across reruns, and the app compares the checkbox's current value against the stored done flag to detect completion. The strikethrough effect is just markdown wrapped in :gray[... emphasis ...] โ no CSS required.
The progress bar uses st.progress(), which takes a fraction from 0.0 to 1.0 โ the same metric-card thinking behind the Pandas dashboard, just pointed at productivity instead of data.
Common Errors & Fixes
tasks.json lives, or use an absolute path with pathlib.Path(__file__).parent.key=; reusing key="check" makes Streamlit treat them as one widget.os.replace() it into place for atomic saves.Key Concepts
st.columns() with width ratios builds compact row layouts.What to Try Next
st.date_input and highlight overdue tasks in red.sqlite3 module for multi-thousand-task scale.st.selectbox โ all / active / completed views.FAQ
Can multiple people use this app at once?
Not safely with a single JSON file โ concurrent writers overwrite each other. For shared lists, move to SQLite with WAL mode or a small database, and give each user their own task list.
How is this different from session state?
st.session_state dies when the browser session ends; a JSON file survives restarts and reboots. Use session state for UI-only values and files or databases for real data.
Can I deploy this so my tasks live in the cloud?
Yes โ but on Streamlit Community Cloud the filesystem is ephemeral and resets on redeploy. Persist tasks in an external store (Supabase, Firebase, or a small hosted database) instead.