{"record":{"id":"67c702374617fcb5","repo":"headroomlabs-ai/headroom","slug":"expected-a-number-got-value-r","errorCode":null,"errorMessage":"expected a number, got {value!r}","messagePattern":"expected a number, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":422,"severity":"error","filePath":"headroom/settings_store.py","lineNumber":776,"sourceCode":"    every type except a plain ``bool``, which becomes ``False``). Raises\n    ``ValueError`` on bad input so callers can surface a per-field message.\n    \"\"\"\n    if value is None:\n        return None\n    if field.type in (\"bool\", \"optional-bool\"):\n        if isinstance(value, bool):\n            return value\n        token = str(value).strip().lower()\n        if field.type == \"optional-bool\" and token == \"\":\n            return None\n        if token in (\"1\", \"true\", \"yes\", \"on\"):\n            return True\n        if token in (\"0\", \"false\", \"no\", \"off\", \"\"):\n            return False\n        raise ValueError(f\"expected a boolean, got {value!r}\")\n    if field.type in (\"int\", \"float\"):\n        if isinstance(value, bool):  # bool is an int subclass — reject explicitly\n            raise ValueError(f\"expected a number, got {value!r}\")\n        number: int | float\n        if field.type == \"int\":\n            if isinstance(value, float) and not value.is_integer():\n                raise ValueError(f\"expected an integer, got {value!r}\")\n            number = int(value)\n        else:\n            number = float(value)\n            if not math.isfinite(number):\n                raise ValueError(f\"expected a finite number, got {value!r}\")\n        if field.minimum is not None and number < field.minimum:\n            raise ValueError(f\"must be >= {field.minimum}\")\n        if field.maximum is not None and number > field.maximum:\n            raise ValueError(f\"must be <= {field.maximum}\")\n        return number\n    if field.type == \"enum\":\n        token = str(value)\n        if token not in field.choices:\n            raise ValueError(f\"{token!r} not one of {list(field.choices)}\")","sourceCodeStart":758,"sourceCodeEnd":794,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/settings_store.py#L758-L794","documentation":"ValueError from _coerce in headroom/settings_store.py when an 'int'/'float' field receives a Python bool. Because bool is a subclass of int in Python, this is rejected explicitly (settings_store.py:776) so that a JSON true/false can never silently become 1/0. The error lands in SettingsValidationError.field_errors under the field's key.","triggerScenarios":"Saving a numeric setting with a JSON boolean, e.g. save({'max_retries': True}) or an env string payload parsed by the caller into True before being handed to the store. Typical when a form toggle is wired to the wrong field name.","commonSituations":"Frontend forms mapping a checkbox to a numeric config field; YAML/env templating where 'true' gets parsed to a bool by a config loader (yaml.safe_load turns unquoted true/1-adjacent values into bools) before reaching the store.","solutions":["Send an actual number instead of a boolean for that field.","If the value comes from YAML/JSON parsing, quote it ('5' not 5-bool mixups) or fix the upstream schema mapping.","Check the field's type in the error message context (the field_errors key tells you which field)."],"exampleFix":"# before\nsave({'max_concurrent': True})  # ValueError: expected a number, got True\n\n# after\nsave({'max_concurrent': 1})","handlingStrategy":"validation","validationCode":"def is_number_like(v) -> bool:\n    return not isinstance(v, bool) and isinstance(v, (int, float))","typeGuard":"from typing import Any\n\ndef is_numeric(v: Any) -> bool:\n    return not isinstance(v, bool) and isinstance(v, (int, float))","tryCatchPattern":"except SettingsValidationError as e:\n    for key, msg in e.field_errors.items():\n        if 'expected a number' in msg and isinstance(payload[key], bool):\n            payload[key] = int(payload[key])\n    store.save(payload)","preventionTips":["Never map boolean UI controls onto numeric settings fields.","In YAML configs, quote numeric-looking values to avoid implicit type coercion."],"tags":["settings","validation","types"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}