{"record":{"id":"a923ed75213dfb18","repo":"shareAI-lab/learn-claude-code","slug":"todos-must-be-a-list","errorCode":null,"errorMessage":"todos must be a list","messagePattern":"todos must be a list","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s05_todo_write/code.py","lineNumber":125,"sourceCode":"\n# -- New in s05: structured state the model updates --\n\nclass TodoManager:\n    def __init__(self):\n        self.items: list[dict] = []\n\n    def update(self, todos: list | str) -> str:\n        if isinstance(todos, str):\n            try:\n                todos = json.loads(todos)\n            except json.JSONDecodeError:\n                try:\n                    todos = ast.literal_eval(todos)\n                except (SyntaxError, ValueError) as e:\n                    raise ValueError(\"todos must be a list or JSON array string\") from e\n\n        if not isinstance(todos, list):\n            raise ValueError(\"todos must be a list\")\n        if len(todos) > 20:\n            raise ValueError(\"Max 20 todos allowed\")\n\n        validated = []\n        in_progress_count = 0\n        for index, todo in enumerate(todos):\n            if not isinstance(todo, dict):\n                raise ValueError(f\"todos[{index}] must be an object\")\n\n            content = str(todo.get(\"content\", \"\")).strip()\n            status = str(todo.get(\"status\", \"pending\")).lower()\n            if not content:\n                raise ValueError(f\"todos[{index}] requires content\")\n            if status not in (\"pending\", \"in_progress\", \"completed\"):\n                raise ValueError(f\"todos[{index}] has invalid status '{status}'\")\n            if status == \"in_progress\":\n                in_progress_count += 1\n            validated.append({\"content\": content, \"status\": status})","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s05_todo_write/code.py#L107-L143","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nTODOS.update({\"content\": \"x\", \"status\": \"pending\"})\n// after\nTODOS.update([{\"content\": \"x\", \"status\": \"pending\"}])","handlingStrategy":"type-guard","validationCode":"if isinstance(todos, dict):\n    todos = [todos]            # single object -> wrap\nelif isinstance(todos, dict) and 'todos' in todos:\n    todos = todos['todos']     # arguments wrapper -> extract\nif not isinstance(todos, list):\n    raise TypeError('expected a list of todos')\nTODOS.update(todos)","typeGuard":"def is_todo_list(v: object) -> bool:\n    return isinstance(v, list) and all(isinstance(x, dict) for x in v)","tryCatchPattern":"try:\n    TODOS.update(todos)\nexcept ValueError as e:\n    if 'must be a list' in str(e) and isinstance(todos, dict):\n        TODOS.update([todos])\n    else:\n        raise","preventionTips":["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"],"tags":["todos","type-validation","agent-tools"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}