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

  1. Convert string elements to objects: [{"content": s, "status": "pending"} for s in todos]
  2. Validate each element with isinstance(todo, dict) before calling and drop/fix offenders
  3. 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

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


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/fcbd8d1ef1e16b1d. Report an issue: GitHub.