shareAI-lab/learn-claude-code · error · ValueError
todos[{index}] must be an object
Error message
todos[{index}] must be an object What it means
Raised by TodoManager.update in s05_todo_write/code.py when an element of the todos list is not a dict. This module does stricter type checking than the s_full variant (which string-coerces via item.get): here non-object elements fail immediately. Common with mixed arrays produced by lenient LLM output, e.g. a bare string alongside proper objects.
Source
Thrown at s05_todo_write/code.py:133
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})
if in_progress_count > 1:
raise ValueError("Only one todo can be in_progress at a time")
self.items = validated
return self.render()
def render(self) -> str:View on GitHub (pinned to 985456f4ad)
Solutions
- Convert string elements to objects: [{"content": s, "status": "pending"} for s in todos]
- Validate each element with isinstance(todo, dict) before calling and drop/fix offenders
- Use the reported index to locate the malformed element in the submitted list
Example fix
// before
TODOS.update(["write tests", "ship"])
// after
TODOS.update([{"content": s, "status": "pending"} for s in ["write tests", "ship"]]) Defensive patterns
Strategy: type-guard
Validate before calling
todos = [t if isinstance(t, dict) else {"content": str(t), "status": "pending"} for t in todos]
assert all(isinstance(t, dict) for t in todos)
TODOS.update(todos) Type guard
def all_objects(lst: list) -> bool:
return all(isinstance(x, dict) for x in lst) Try / catch
try:
TODOS.update(todos)
except ValueError as e:
m = re.search(r"todos\[(\d+)\] must be an object", str(e))
if m:
i = int(m.group(1))
todos[i] = {"content": str(todos[i]), "status": "pending"}
TODOS.update(todos)
else:
raise Prevention
- Coerce string elements to {content, status} objects before calling
- Use a strict schema (type: array, items: type: object) in the tool definition
- Reject heterogeneous arrays early in your own layer
When it happens
Trigger: update(["write tests", {"content": "ship", "status": "pending"}]) — element 0 is a str; a JSON string like '["step 1", "step 2"]' (list of strings) parsed from a caller that assumed plain-text todos were supported.
Common situations: LLMs simplifying the schema by sending plain strings; callers migrating from a checklist API that accepted strings; heterogeneous arrays where one malformed element poisons the batch.
Related errors
- todos must be a list
- 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/fcbd8d1ef1e16b1d.
Report an issue: GitHub.