shareAI-lab/learn-claude-code · error · ValueError
todos must be a list or JSON array string
Error message
todos must be a list or JSON array string
What it means
Raised by TodoManager.update in s05_todo_write/code.py when the todos argument is a string that is neither parseable JSON nor parseable by ast.literal_eval. The module accepts both formats because LLM tool callers frequently emit Python-literal lists with single quotes; when both parsers fail, this wrapper error is raised with the original parse error chained as __cause__.
Source
Thrown at s05_todo_write/code.py:122
except Exception as e:
return f"Error: {e}"
# -- 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}'")View on GitHub (pinned to 985456f4ad)
Solutions
- Pass the list as a native JSON array (the tool schema's array type) instead of a string
- Strip markdown fences and surrounding prose before calling: s.strip().strip('`').removeprefix('json')
- Inspect e.__cause__ to see the exact parse location and fix that character
- Validate client-side with json.loads first and retry generation on failure
Example fix
// before
TODOS.update("```json\n[{\"content\": \"x\", \"status\": \"pending\"}]\n```")
// after
TODOS.update([{"content": "x", "status": "pending"}]) Defensive patterns
Strategy: validation
Validate before calling
import json
def parse_todos(raw):
if isinstance(raw, str):
s = raw.strip()
if s.startswith('```'):
s = s.strip('`').removeprefix('json').strip()
raw = json.loads(s) # raises early with a clear JSON error
assert isinstance(raw, list)
return raw Try / catch
try:
TODOS.update(raw)
except ValueError as e:
if 'must be a list or JSON array' in str(e):
raw = json.loads(strip_fences(raw))
TODOS.update(raw)
else:
raise Prevention
- Prefer passing a native array through the tool schema over a string
- Strip markdown fences from LLM output before parsing
- Inspect __cause__ for the exact syntax error position
When it happens
Trigger: Calling update with '[{"content": "x",]' (trailing comma, invalid JSON and invalid Python), a string containing markdown fences like "```json [...]"", truncated JSON cut off by an output token limit, or a bare word like "none".
Common situations: LLM tool calls wrapping the array in prose or code fences; single quotes handled fine by literal_eval but mixed quoting ("content": 'x') failing both parsers; response truncation producing half a JSON array.
Related errors
- Item {i}: content required
- Item {i}: invalid status '{status}'
- todos[{index}] requires content
- Max 20 todos allowed
- Item {item_id}: text required
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/052ff9f341a9cf37.
Report an issue: GitHub.