A TodoList class that saves itself to JSON and loads back exactly as it was.
/build/project-todo-manager-json with the starter and the rubric already in it.Write a TodoList class with a Task dataclass: add tasks with priorities, complete and remove them, save the list to todos.json, load it into a new object with ids intact, list what is pending by priority, and search by word.
Every tutorial has a to-do app; this one is graded on what they skip: refusing bad input, raising the right error for a missing id, and not reusing ids after a reload. That is the difference between a class that works once and one that survives a restart.
You can design a small class, use a dataclass, persist state as JSON and restore it without losing anything.
Each step is one function in todo.py. In the workspace, press Check next to a step: it runs your file and tells you exactly what is still wrong.
Build against this exact list — it is the spec your published project is judged on.
Copy this into a new file — or open it in a workspace as todo.py. The TODO parts are yours to fill.
"""
To-do manager: a class that saves itself to JSON.
A TodoList you can add to, tick off, save to todos.json and load back.
The demo in main() starts from a clean file every run, saves, then loads
the list into a brand-new object to prove the data survived.
Fill in one method per step, press Run, then press "Check step".
"""
import json
from dataclasses import asdict, dataclass
from pathlib import Path
PATH = Path("todos.json")
PRIORITIES = {"high": 0, "medium": 1, "low": 2}
@dataclass
class Task:
id: int
title: str
priority: str = "medium"
done: bool = False
class TodoList:
def __init__(self, path: Path = PATH):
self.path = Path(path)
self.tasks: dict[int, Task] = {}
self.next_id = 1
def __len__(self) -> int:
return len(self.tasks)
def add(self, title: str, priority: str = "medium") -> int:
# TODO: strip the title; ValueError if it is empty or if priority is
# not a key of PRIORITIES. Create Task(self.next_id, title, priority),
# store it in self.tasks under its id, bump self.next_id, return the id.
raise NotImplementedError("step 1, add(): create a task with the next id")
def complete(self, task_id: int) -> None:
# TODO: mark the task done. KeyError if there is no such id.
raise NotImplementedError("step 2, complete(): tick a task off")
def remove(self, task_id: int) -> None:
# TODO: delete the task. KeyError if there is no such id.
raise NotImplementedError("step 2, remove(): delete a task")
def save(self) -> None:
# TODO: write {"next_id": ..., "tasks": [asdict(t), ...]} to self.path
# as JSON. Hint: self.path.write_text(json.dumps(data, indent=2))
raise NotImplementedError("step 3, save(): write the list to JSON")
@classmethod
def load(cls, path: Path = PATH) -> "TodoList":
# TODO: build cls(path). If the file is missing, return it empty.
# Otherwise read the JSON, rebuild each Task(**row) and restore next_id
# (so ids never repeat after a reload).
raise NotImplementedError("step 4, load(): rebuild the list from JSON")
def pending(self) -> list[Task]:
# TODO: tasks that are not done, high before medium before low, and
# by id inside the same priority. Hint: key=lambda t: (PRIORITIES[t.priority], t.id)
raise NotImplementedError("step 5, pending(): open tasks, most urgent first")
def search(self, word: str) -> list[Task]:
# TODO: tasks whose title contains word, ignoring upper/lower case.
raise NotImplementedError("step 6, search(): find tasks by a word")
def main() -> None:
if PATH.exists():
PATH.unlink() # the demo starts clean every run
todo = TodoList(PATH)
todo.add("Revise DSA: two pointers", "high")
todo.add("Submit resume on placement portal", "high")
todo.add("Buy notebook", "low")
todo.add("Practise 2 SQL joins")
call = todo.add("Call home", "low")
todo.complete(2)
todo.remove(call)
todo.save()
print(f"Saved {len(todo)} tasks to {PATH}")
again = TodoList.load(PATH)
done = sum(t.done for t in again.tasks.values())
print(f"Reloaded {len(again)} tasks, {done} done")
print("Pending:")
for t in again.pending():
print(f" #{t.id} {'[' + t.priority + ']':<8} {t.title}")
print(f"Next id: {again.next_id}")
hits = again.search("dsa")
print(f"Search 'dsa': {', '.join(f'#{t.id} {t.title}' for t in hits)}")
if __name__ == "__main__":
try:
main()
except NotImplementedError as todo:
# A fresh starter is SUPPOSED to stop here. Say which step is next
# instead of printing a traceback that looks like a bug.
print(f"Not built yet: {todo}")
print("Write that method, then press Run again. Each step you finish moves this message forward.")
Try every step first. This version passes all 6 checks; yours can look different and still pass.
"""
To-do manager: a class that saves itself to JSON.
A TodoList you can add to, tick off, save to todos.json and load back.
The demo in main() starts from a clean file every run, saves, then loads
the list into a brand-new object to prove the data survived.
"""
import json
from dataclasses import asdict, dataclass
from pathlib import Path
PATH = Path("todos.json")
PRIORITIES = {"high": 0, "medium": 1, "low": 2}
@dataclass
class Task:
id: int
title: str
priority: str = "medium"
done: bool = False
class TodoList:
def __init__(self, path: Path = PATH):
self.path = Path(path)
self.tasks: dict[int, Task] = {}
self.next_id = 1
def __len__(self) -> int:
return len(self.tasks)
def add(self, title: str, priority: str = "medium") -> int:
title = title.strip()
if not title:
raise ValueError("title cannot be empty")
if priority not in PRIORITIES:
raise ValueError(f"priority must be one of {list(PRIORITIES)}")
task = Task(self.next_id, title, priority)
self.tasks[task.id] = task
self.next_id += 1
return task.id
def complete(self, task_id: int) -> None:
if task_id not in self.tasks:
raise KeyError(f"no task #{task_id}")
self.tasks[task_id].done = True
def remove(self, task_id: int) -> None:
if task_id not in self.tasks:
raise KeyError(f"no task #{task_id}")
del self.tasks[task_id]
def save(self) -> None:
data = {"next_id": self.next_id, "tasks": [asdict(t) for t in self.tasks.values()]}
self.path.write_text(json.dumps(data, indent=2), encoding="utf-8")
@classmethod
def load(cls, path: Path = PATH) -> "TodoList":
todo = cls(path)
if not todo.path.exists():
return todo
data = json.loads(todo.path.read_text(encoding="utf-8"))
for row in data["tasks"]:
task = Task(**row)
todo.tasks[task.id] = task
todo.next_id = data["next_id"]
return todo
def pending(self) -> list[Task]:
open_tasks = [t for t in self.tasks.values() if not t.done]
return sorted(open_tasks, key=lambda t: (PRIORITIES[t.priority], t.id))
def search(self, word: str) -> list[Task]:
word = word.lower()
return [t for t in self.tasks.values() if word in t.title.lower()]
def main() -> None:
if PATH.exists():
PATH.unlink() # the demo starts clean every run
todo = TodoList(PATH)
todo.add("Revise DSA: two pointers", "high")
todo.add("Submit resume on placement portal", "high")
todo.add("Buy notebook", "low")
todo.add("Practise 2 SQL joins")
call = todo.add("Call home", "low")
todo.complete(2)
todo.remove(call)
todo.save()
print(f"Saved {len(todo)} tasks to {PATH}")
again = TodoList.load(PATH)
done = sum(t.done for t in again.tasks.values())
print(f"Reloaded {len(again)} tasks, {done} done")
print("Pending:")
for t in again.pending():
print(f" #{t.id} {'[' + t.priority + ']':<8} {t.title}")
print(f"Next id: {again.next_id}")
hits = again.search("dsa")
print(f"Search 'dsa': {', '.join(f'#{t.id} {t.title}' for t in hits)}")
if __name__ == "__main__":
main()
todo.py and a README.md holding all 5 rubric items as a checklist.pyrun.in/u/<handle>/w/project-todo-manager-json.pyrun.in/u/<handle> portfolio page.Your workspace saves to this browser only. Sign in to keep it across devices and to publish it.
Flip the workspace to Public and paste its URL below, or push the file to a GitHub gist or repo. Paste the code inline too if you want the optional AI review to comment on specific lines.
A published PyRun workspace URL (pyrun.in/u/<handle>/w/project-todo-manager-json) works as the public URL too.