shareAI-lab/learn-claude-code · error · ValueError
todos must be a list
Error message
todos must be a list
What it means
Raised by TodoManager.update in s05_todo_write/code.py when, after optional string parsing, the todos value is not a Python list. Unlike error 30 this fires when the value parsed successfully (or was passed natively) but has the wrong top-level type — typically a dict (single object) or a string like "pending" that literal_eval accepts as a non-list value.
Source
Thrown at s05_todo_write/code.py:125
# -- New in s05: structured state the model updates --
class TodoManager:
def __init__(self):
self.items: list[dict] = []
def update(self, todos: list | str) -> str:
if isinstance(todos, str):
try:
todos = json.loads(todos)
except json.JSONDecodeError:
try:
todos = ast.literal_eval(todos)
except (SyntaxError, ValueError) as e:
raise ValueError("todos must be a list or JSON array string") from e
if not isinstance(todos, list):
raise ValueError("todos must be a list")
if len(todos) > 20:
raise ValueError("Max 20 todos allowed")
validated = []
in_progress_count = 0
for index, todo in enumerate(todos):
if not isinstance(todo, dict):
raise ValueError(f"todos[{index}] must be an object")
content = str(todo.get("content", "")).strip()
status = str(todo.get("status", "pending")).lower()
if not content:
raise ValueError(f"todos[{index}] requires content")
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"todos[{index}] has invalid status '{status}'")
if status == "in_progress":
in_progress_count += 1
validated.append({"content": content, "status": status})View on GitHub (pinned to 985456f4ad)
Solutions
- Wrap single items in a list: update([todo]) not update(todo)
- Extract the array field from the arguments object before calling: args['todos'], not args
- Check isinstance(todos, list) client-side and re-shape before the call
Example fix
// before
TODOS.update({"content": "x", "status": "pending"})
// after
TODOS.update([{"content": "x", "status": "pending"}]) Defensive patterns
Strategy: type-guard
Validate before calling
if isinstance(todos, dict):
todos = [todos] # single object -> wrap
elif isinstance(todos, dict) and 'todos' in todos:
todos = todos['todos'] # arguments wrapper -> extract
if not isinstance(todos, list):
raise TypeError('expected a list of todos')
TODOS.update(todos) Type guard
def is_todo_list(v: object) -> bool:
return isinstance(v, list) and all(isinstance(x, dict) for x in v) Try / catch
try:
TODOS.update(todos)
except ValueError as e:
if 'must be a list' in str(e) and isinstance(todos, dict):
TODOS.update([todos])
else:
raise Prevention
- Wrap single todo objects in an array before calling
- Extract the array field from argument objects, do not pass the whole object
- Type-check the top-level shape before submission
When it happens
Trigger: Passing a single todo object {"content": ...} instead of a list wrapping it; passing a JSON object {"todos": [...]} (a dict); passing "'pending'" which literal_eval parses to a str; passing an int or None.
Common situations: LLMs omitting the outer array for single-item updates; nested schemas where the caller passes the whole arguments object rather than the todos field; string inputs that are valid Python literals of the wrong type.
Related errors
- todos[{index}] must be an object
- Item {i}: content required
- Item {i}: invalid status '{status}'
- todos must be a list or JSON array string
- todos[{index}] requires content
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/a923ed75213dfb18.
Report an issue: GitHub.