headroomlabs-ai/headroom · error · ValueError

expected a number, got {value!r}

Error message

expected a number, got {value!r}

What it means

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.

Source

Thrown at headroom/settings_store.py:776

    every type except a plain ``bool``, which becomes ``False``). Raises
    ``ValueError`` on bad input so callers can surface a per-field message.
    """
    if value is None:
        return None
    if field.type in ("bool", "optional-bool"):
        if isinstance(value, bool):
            return value
        token = str(value).strip().lower()
        if field.type == "optional-bool" and token == "":
            return None
        if token in ("1", "true", "yes", "on"):
            return True
        if token in ("0", "false", "no", "off", ""):
            return False
        raise ValueError(f"expected a boolean, got {value!r}")
    if field.type in ("int", "float"):
        if isinstance(value, bool):  # bool is an int subclass — reject explicitly
            raise ValueError(f"expected a number, got {value!r}")
        number: int | float
        if field.type == "int":
            if isinstance(value, float) and not value.is_integer():
                raise ValueError(f"expected an integer, got {value!r}")
            number = int(value)
        else:
            number = float(value)
            if not math.isfinite(number):
                raise ValueError(f"expected a finite number, got {value!r}")
        if field.minimum is not None and number < field.minimum:
            raise ValueError(f"must be >= {field.minimum}")
        if field.maximum is not None and number > field.maximum:
            raise ValueError(f"must be <= {field.maximum}")
        return number
    if field.type == "enum":
        token = str(value)
        if token not in field.choices:
            raise ValueError(f"{token!r} not one of {list(field.choices)}")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Send an actual number instead of a boolean for that field.
  2. If the value comes from YAML/JSON parsing, quote it ('5' not 5-bool mixups) or fix the upstream schema mapping.
  3. Check the field's type in the error message context (the field_errors key tells you which field).

Example fix

# before
save({'max_concurrent': True})  # ValueError: expected a number, got True

# after
save({'max_concurrent': 1})
Defensive patterns

Strategy: validation

Validate before calling

def is_number_like(v) -> bool:
    return not isinstance(v, bool) and isinstance(v, (int, float))

Type guard

from typing import Any

def is_numeric(v: Any) -> bool:
    return not isinstance(v, bool) and isinstance(v, (int, float))

Try / catch

except SettingsValidationError as e:
    for key, msg in e.field_errors.items():
        if 'expected a number' in msg and isinstance(payload[key], bool):
            payload[key] = int(payload[key])
    store.save(payload)

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/67c702374617fcb5. Report an issue: GitHub.