This project adds the thing most beginner programs are missing: data that survives closing the program. You will store tasks in a JSON file and load them back on the next run.
What you will build#
TO-DO (2 of 3 done)
1. [x] Buy milk
2. [ ] Finish Python guide
3. [x] Call the bank
a) add d) done r) remove q) quit
Choice:Step 1: decide the shape of your data#
Before writing any code, decide what one task looks like:
{"text": "Buy milk", "done": False}And the whole list is simply a list of those:
tasks = [
{"text": "Buy milk", "done": True},
{"text": "Finish Python guide", "done": False},
]Getting this shape right first makes everything after it easier. See Python dictionaries explained.
Step 2: saving and loading#
import json
FILE = "tasks.json"
def load_tasks():
try:
with open(FILE) as f:
return json.load(f)
except FileNotFoundError:
return [] # first run — start empty
except json.JSONDecodeError:
print("tasks.json is damaged. Starting fresh.")
return []
def save_tasks(tasks):
with open(FILE, "w") as f:
json.dump(tasks, f, indent=2)Two failure cases are handled: the file not existing yet, and the file being corrupted. Both would otherwise crash on startup.
Step 3: the operations#
def add_task(tasks, text):
text = text.strip()
if not text:
return "Task cannot be empty."
tasks.append({"text": text, "done": False})
return f"Added: {text}"
def toggle_task(tasks, number):
if number < 1 or number > len(tasks):
return "No task with that number."
task = tasks[number - 1]
task["done"] = not task["done"]
return ("Done: " if task["done"] else "Reopened: ") + task["text"]
def remove_task(tasks, number):
if number < 1 or number > len(tasks):
return "No task with that number."
removed = tasks.pop(number - 1)
return f"Removed: {removed['text']}"Each returns a message instead of printing. That keeps them testable and means all output happens in one place.
Note number - 1: people count from 1, lists count from 0. Converting once at the boundary is much safer than remembering everywhere.
Step 4: showing the list#
def show_tasks(tasks):
if not tasks:
print("
Nothing to do. Add something with 'a'.")
return
done = sum(1 for t in tasks if t["done"])
print(f"
TO-DO ({done} of {len(tasks)} done)")
for i, task in enumerate(tasks, start=1):
mark = "x" if task["done"] else " "
print(f" {i}. [{mark}] {task['text']}")The finished program#
import json
FILE = "tasks.json"
def load_tasks():
try:
with open(FILE) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
def save_tasks(tasks):
with open(FILE, "w") as f:
json.dump(tasks, f, indent=2)
def add_task(tasks, text):
text = text.strip()
if not text:
return "Task cannot be empty."
tasks.append({"text": text, "done": False})
return f"Added: {text}"
def toggle_task(tasks, number):
if number < 1 or number > len(tasks):
return "No task with that number."
task = tasks[number - 1]
task["done"] = not task["done"]
return ("Done: " if task["done"] else "Reopened: ") + task["text"]
def remove_task(tasks, number):
if number < 1 or number > len(tasks):
return "No task with that number."
return f"Removed: {tasks.pop(number - 1)['text']}"
def show_tasks(tasks):
if not tasks:
print("
Nothing to do. Add something with 'a'.")
return
done = sum(1 for t in tasks if t["done"])
print(f"
TO-DO ({done} of {len(tasks)} done)")
for i, task in enumerate(tasks, start=1):
mark = "x" if task["done"] else " "
print(f" {i}. [{mark}] {task['text']}")
def ask_number(prompt):
try:
return int(input(prompt))
except ValueError:
return -1
def main():
tasks = load_tasks()
while True:
show_tasks(tasks)
choice = input("
a) add d) done r) remove q) quit
Choice: ").strip().lower()
if choice == "q":
save_tasks(tasks)
print("Saved. Goodbye.")
break
elif choice == "a":
print(add_task(tasks, input("Task: ")))
elif choice == "d":
print(toggle_task(tasks, ask_number("Number: ")))
elif choice == "r":
print(remove_task(tasks, ask_number("Number: ")))
else:
print("Unknown choice.")
save_tasks(tasks)
main()
How the code works#
- Saving after every change means an unexpected crash never loses data.
enumerate(tasks, start=1)gives human-friendly numbering in one step.sum(1 for t in tasks if t["done"])counts matching items without a loop body.ask_numberreturning -1 on bad input lets the validation live in one place.json.dump(..., indent=2)writes a file you can open and read yourself.
Questions people ask#
Why JSON rather than a plain text file?
JSON keeps the structure. A text file would need you to invent and parse your own format for the done flag, which is exactly the work JSON does for you.
Where is tasks.json saved?
In the folder you ran the command from, not necessarily where the script lives. Run import os; print(os.getcwd()) if it appears somewhere unexpected.
Could I use a database instead?
Yes, and Python includes sqlite3. JSON is the right tool at this size; move to SQLite when you have thousands of records or need to search them.
Where to go next#
- Build the same app in the browser
- Build a quiz app
- Python try / except explained — the error handling this project uses
- Reading and writing files in Python — the file work behind this project